Version 3.1 InDev
60
V3.1/build/opt/jibo/Jibo/Skills/@be/be/be/robot-logger.js
Normal file
@@ -0,0 +1,60 @@
|
||||
"use strict";
|
||||
|
||||
// Minimal UDP logger client for old robot environments.
|
||||
// Safe to require from any module; if logd is not running, it will just no-op.
|
||||
|
||||
const dgram = require("dgram");
|
||||
|
||||
const DEFAULT_HOST = process.env.JIBO_LOGD_HOST || "127.0.0.1";
|
||||
const DEFAULT_PORT = parseInt(process.env.JIBO_LOGD_PORT || "15140", 10);
|
||||
|
||||
let _socket = null;
|
||||
|
||||
function getSocket() {
|
||||
if (_socket) return _socket;
|
||||
try {
|
||||
_socket = dgram.createSocket("udp4");
|
||||
_socket.unref();
|
||||
return _socket;
|
||||
} catch (e) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
function send(obj) {
|
||||
const sock = getSocket();
|
||||
if (!sock) return;
|
||||
|
||||
let payload;
|
||||
try {
|
||||
payload = Buffer.from(JSON.stringify(obj));
|
||||
} catch (e) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
sock.send(payload, 0, payload.length, DEFAULT_PORT, DEFAULT_HOST, function () { });
|
||||
} catch (e) {
|
||||
// ignore
|
||||
}
|
||||
}
|
||||
|
||||
function mk(tag, level, msg, data) {
|
||||
const o = { tag: tag || "skill", level: level || "info", msg: String(msg || "") };
|
||||
if (typeof data !== "undefined") o.data = data;
|
||||
return o;
|
||||
}
|
||||
|
||||
exports.info = function (tag, msg, data) { send(mk(tag, "info", msg, data)); };
|
||||
exports.warn = function (tag, msg, data) { send(mk(tag, "warn", msg, data)); };
|
||||
exports.error = function (tag, msg, data) { send(mk(tag, "error", msg, data)); };
|
||||
exports.debug = function (tag, msg, data) { send(mk(tag, "debug", msg, data)); };
|
||||
|
||||
exports.raw = function (line) {
|
||||
const sock = getSocket();
|
||||
if (!sock) return;
|
||||
const payload = Buffer.from(String(line || ""));
|
||||
try {
|
||||
sock.send(payload, 0, payload.length, DEFAULT_PORT, DEFAULT_HOST, function () { });
|
||||
} catch (e) { }
|
||||
};
|
||||
65
V3.1/build/opt/jibo/Jibo/Skills/@be/be/menu/PROVIDERS.md
Normal file
@@ -0,0 +1,65 @@
|
||||
# Menu providers (drop-in menu customization)
|
||||
|
||||
Providers let you add/override menu entries without editing the core menu patch.
|
||||
|
||||
Default entries directory (on robot):
|
||||
- `/opt/jibo/Jibo/Skills/@be/menu-entries.d/`
|
||||
|
||||
Legacy fallback (yea ik its the same stop talking):
|
||||
- `/opt/jibo/Jibo/Skills/@be/menu-providers.d/`
|
||||
|
||||
## Provider file types
|
||||
|
||||
### JSON provider (`*.json`)
|
||||
Supported shapes:
|
||||
|
||||
- An array of entries: `[ { ... }, { ... } ]`
|
||||
- Or `{ "entries": [ ... ] }`
|
||||
|
||||
Entry schema (same as `menuEntry.json` scan output):
|
||||
|
||||
- `id` (string, required)
|
||||
- `type` (`skill` or `submenu`)
|
||||
- `title`
|
||||
- `icon`
|
||||
- `color`
|
||||
- `description`
|
||||
- `order` (number)
|
||||
- `skillId` (for type `skill`)
|
||||
- `submenuTitle` (for type `submenu`)
|
||||
- `children` (array of skill entries, for type `submenu`)
|
||||
- `childrenDir` (for type `submenu`, optional):
|
||||
- Absolute path (starts with `/`) or relative to `skillsRoot`
|
||||
- If provided and `children` is missing/empty, the patch will scan this directory for child skills (subfolders containing `menuEntry.json`).
|
||||
|
||||
Example submenu that lists a directory:
|
||||
|
||||
```json
|
||||
[
|
||||
{
|
||||
"id": "fun_stuff",
|
||||
"type": "submenu",
|
||||
"title": "Fun Stuff",
|
||||
"icon": "resources/icons/fun-stuff.png",
|
||||
"order": 20,
|
||||
"childrenDir": "FunStuff"
|
||||
}
|
||||
]
|
||||
```
|
||||
|
||||
### JS provider (`*.js`)
|
||||
Exports one of:
|
||||
|
||||
- `module.exports = function(ctx) { return [ ...entries... ]; }`
|
||||
- `exports.getEntries = function(ctx) { return [ ...entries... ]; }`
|
||||
- `exports.entries = [ ...entries... ]`
|
||||
|
||||
`ctx` includes:
|
||||
- `skillsRoot`
|
||||
- `providersDir`
|
||||
- `log` (function)
|
||||
|
||||
## Conflict rules
|
||||
|
||||
- If a provider entry has the same `id` as a scanned entry, the provider entry wins.
|
||||
- Sorting: by `order` then by title.
|
||||
@@ -12,7 +12,7 @@ When Jibo boots up, the `main-menu-patch.js` module:
|
||||
|
||||
## Quick Start
|
||||
|
||||
### Adding a Simple Skill Button to the Menu
|
||||
### Adding a Simple Skill Button to the Menu (you really should follow PROVIDERS.md at this point)
|
||||
|
||||
1. Create a folder in the `Skills` directory (e.g., `MySkill/`)
|
||||
2. Add a `menuEntry.json` file inside:
|
||||
@@ -53,7 +53,7 @@ When Jibo boots up, the `main-menu-patch.js` module:
|
||||
3. Inside that folder, create subfolders for each skill, each with their own `menuEntry.json`
|
||||
4. A button will appear in the main menu that opens a submenu with those skills!
|
||||
|
||||
## Directory Structure Example
|
||||
## Directory Structure Example (OLD)
|
||||
|
||||
```
|
||||
/opt/jibo/Jibo/Skills/
|
||||
|
||||
@@ -10,11 +10,38 @@
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
|
||||
// Skills root directory
|
||||
const SKILLS_ROOT = '/opt/jibo/Jibo/Skills';
|
||||
const menuEntries = require('./menu-entries');
|
||||
const skillsRootUtil = require('./skills-root');
|
||||
|
||||
function strStartsWith(s, prefix) {
|
||||
return (typeof s === 'string') && (s.indexOf(prefix) === 0);
|
||||
}
|
||||
|
||||
function strEndsWith(s, suffix) {
|
||||
if (typeof s !== 'string' || typeof suffix !== 'string') return false;
|
||||
if (suffix.length > s.length) return false;
|
||||
return s.indexOf(suffix, s.length - suffix.length) !== -1;
|
||||
}
|
||||
|
||||
function strIncludes(s, needle) {
|
||||
return (typeof s === 'string') && (s.indexOf(needle) !== -1);
|
||||
}
|
||||
|
||||
// Optional UDP logger (Python logd). Safe fallback if unavailable.
|
||||
let robotLogger = null;
|
||||
try {
|
||||
robotLogger = require('../be/robot-logger');
|
||||
} catch (e) {
|
||||
robotLogger = null;
|
||||
}
|
||||
|
||||
// Skills root directory (configurable)
|
||||
let SKILLS_ROOT = skillsRootUtil.resolveSkillsRoot();
|
||||
|
||||
// Cache for scanned skills
|
||||
let _cachedMenuEntries = null;
|
||||
let _cachedSkillsRoot = null;
|
||||
let _cachedProvidersDir = null;
|
||||
let _submenuConfigs = {};
|
||||
let _dynamicSkillIds = new Set(); // Track dynamic skill IDs for routing
|
||||
let _beInstance = null;
|
||||
@@ -40,6 +67,11 @@ const LOG_FILE = '/tmp/menu-patch.log';
|
||||
function log(msg, ...args) {
|
||||
const line = '[MENU-PATCH] ' + msg + ' ' + args.map(a => typeof a === 'object' ? JSON.stringify(a) : String(a)).join(' ');
|
||||
console.log(line);
|
||||
try {
|
||||
if (robotLogger && typeof robotLogger.raw === 'function') {
|
||||
robotLogger.raw(line);
|
||||
}
|
||||
} catch (e) { }
|
||||
try {
|
||||
fs.appendFileSync(LOG_FILE, new Date().toISOString() + ' ' + line + '\n');
|
||||
} catch (e) {}
|
||||
@@ -48,6 +80,11 @@ function log(msg, ...args) {
|
||||
function warn(msg, ...args) {
|
||||
const line = '[MENU-PATCH WARN] ' + msg + ' ' + args.map(a => typeof a === 'object' ? JSON.stringify(a) : String(a)).join(' ');
|
||||
console.warn(line);
|
||||
try {
|
||||
if (robotLogger && typeof robotLogger.raw === 'function') {
|
||||
robotLogger.raw(line);
|
||||
}
|
||||
} catch (e) { }
|
||||
try {
|
||||
fs.appendFileSync(LOG_FILE, new Date().toISOString() + ' ' + line + '\n');
|
||||
} catch (e) {}
|
||||
@@ -172,59 +209,37 @@ function scanSubmenuChildren(submenuDir) {
|
||||
* Scan the Skills root directory for all menu entries
|
||||
*/
|
||||
function scanAllMenuEntries() {
|
||||
if (_cachedMenuEntries) {
|
||||
if (_cachedMenuEntries && _cachedSkillsRoot === SKILLS_ROOT) {
|
||||
log('Returning cached entries:', _cachedMenuEntries.length);
|
||||
return _cachedMenuEntries;
|
||||
}
|
||||
|
||||
|
||||
log('Scanning Skills directory:', SKILLS_ROOT);
|
||||
|
||||
_dynamicSkillIds.clear();
|
||||
const entries = [];
|
||||
let children;
|
||||
|
||||
// Check if directory exists
|
||||
if (!fs.existsSync(SKILLS_ROOT)) {
|
||||
warn('Skills directory does not exist:', SKILLS_ROOT);
|
||||
return entries;
|
||||
}
|
||||
|
||||
|
||||
const res = menuEntries.getMenuEntries({
|
||||
skillsRoot: SKILLS_ROOT,
|
||||
log: function () {
|
||||
try { log.apply(null, arguments); } catch (e) { }
|
||||
}
|
||||
});
|
||||
|
||||
_cachedMenuEntries = res.entries || [];
|
||||
_cachedSkillsRoot = res.skillsRoot;
|
||||
_cachedProvidersDir = res.providersDir;
|
||||
|
||||
// update dynamic id set
|
||||
try {
|
||||
children = fs.readdirSync(SKILLS_ROOT);
|
||||
log('Found', children.length, 'items in Skills directory:', children.join(', '));
|
||||
} catch (e) {
|
||||
warn('Failed to read skills directory:', SKILLS_ROOT, e.message);
|
||||
return entries;
|
||||
if (res.dynamicSkillIds && typeof res.dynamicSkillIds.forEach === 'function') {
|
||||
res.dynamicSkillIds.forEach(function (id) { _dynamicSkillIds.add(id); });
|
||||
}
|
||||
} catch (e) { }
|
||||
|
||||
log('Total entries found:', _cachedMenuEntries.length);
|
||||
if (_cachedProvidersDir) {
|
||||
log('Providers dir:', _cachedProvidersDir);
|
||||
}
|
||||
|
||||
children.forEach(name => {
|
||||
if (name.startsWith('.') || name === '@be' || name === 'node_modules') {
|
||||
log('Skipping:', name);
|
||||
return;
|
||||
}
|
||||
|
||||
const skillDir = path.join(SKILLS_ROOT, name);
|
||||
if (!isDirectory(skillDir)) {
|
||||
log('Not a directory:', name);
|
||||
return;
|
||||
}
|
||||
|
||||
const entry = scanSkillEntry(skillDir, name);
|
||||
if (entry) {
|
||||
entries.push(entry);
|
||||
log('Added entry:', entry.title, '(type:', entry.type + ')');
|
||||
}
|
||||
});
|
||||
|
||||
// Sort by order, then alphabetically
|
||||
entries.sort((a, b) => {
|
||||
if (a.order !== b.order) return a.order - b.order;
|
||||
return a.title.localeCompare(b.title);
|
||||
});
|
||||
|
||||
log('Total entries found:', entries.length);
|
||||
_cachedMenuEntries = entries;
|
||||
return entries;
|
||||
return _cachedMenuEntries;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -427,7 +442,7 @@ function patchMainMenuSkill(be) {
|
||||
}
|
||||
|
||||
// Handle dynamic submenus
|
||||
if (skill && skill.startsWith('dynamic-submenu-')) {
|
||||
if (skill && strStartsWith(skill, 'dynamic-submenu-')) {
|
||||
log('Showing submenu:', skill);
|
||||
const submenuConfig = _submenuConfigs[skill];
|
||||
if (submenuConfig && _jibo && _jibo.face && _jibo.face.views) {
|
||||
@@ -443,11 +458,15 @@ function patchMainMenuSkill(be) {
|
||||
if (result && result.on) {
|
||||
result.on('select', (selection) => {
|
||||
log('Submenu item selected:', selection);
|
||||
if (selection && selection.action && selection.action.data) {
|
||||
const destination = selection.action.data.utterance?.entities?.destination;
|
||||
if (destination) {
|
||||
this.redirectToSkill(destination);
|
||||
try {
|
||||
if (selection && selection.action && selection.action.data && selection.action.data.utterance && selection.action.data.utterance.entities) {
|
||||
const destination = selection.action.data.utterance.entities.destination;
|
||||
if (destination) {
|
||||
this.redirectToSkill(destination);
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
warn('Submenu selection handler error:', e && e.message ? e.message : e);
|
||||
}
|
||||
});
|
||||
}
|
||||
@@ -459,7 +478,7 @@ function patchMainMenuSkill(be) {
|
||||
}
|
||||
|
||||
// Handle dynamic skills (prefixed with 'dynamic:')
|
||||
if (skill && skill.startsWith('dynamic:')) {
|
||||
if (skill && strStartsWith(skill, 'dynamic:')) {
|
||||
const actualSkillId = skill.substring(8); // Remove 'dynamic:' prefix
|
||||
log('Launching dynamic skill:', actualSkillId);
|
||||
|
||||
@@ -530,6 +549,14 @@ function applyPatch(be) {
|
||||
function patchLoader(jibo, skillsRoot) {
|
||||
log('patchLoader called');
|
||||
_jibo = jibo;
|
||||
|
||||
// Allow caller to override skills root.
|
||||
if (skillsRoot && typeof skillsRoot === 'string') {
|
||||
SKILLS_ROOT = skillsRoot;
|
||||
} else {
|
||||
SKILLS_ROOT = skillsRootUtil.resolveSkillsRoot();
|
||||
}
|
||||
clearCache();
|
||||
|
||||
if (!jibo) {
|
||||
warn('jibo is null/undefined');
|
||||
@@ -553,7 +580,7 @@ function patchLoader(jibo, skillsRoot) {
|
||||
log('>>> jibo.loader.load called with:', resourcePath);
|
||||
|
||||
// Check if this is loading a dynamic submenu
|
||||
if (resourcePath && resourcePath.startsWith('dynamic-submenu-')) {
|
||||
if (resourcePath && strStartsWith(resourcePath, 'dynamic-submenu-')) {
|
||||
const submenuConfig = _submenuConfigs[resourcePath];
|
||||
if (submenuConfig) {
|
||||
log('Loading dynamic submenu:', resourcePath);
|
||||
@@ -566,10 +593,10 @@ function patchLoader(jibo, skillsRoot) {
|
||||
|
||||
// Check if this is the main menu - match various possible paths
|
||||
const isMainMenu = resourcePath && (
|
||||
resourcePath.includes('main-menu-verbal.json') ||
|
||||
resourcePath.includes('main-menu.json') ||
|
||||
strIncludes(resourcePath, 'main-menu-verbal.json') ||
|
||||
strIncludes(resourcePath, 'main-menu.json') ||
|
||||
resourcePath === 'resources/views/main-menu-verbal.json' ||
|
||||
resourcePath.endsWith('main-menu-verbal.json')
|
||||
strEndsWith(resourcePath, 'main-menu-verbal.json')
|
||||
);
|
||||
|
||||
if (isMainMenu) {
|
||||
@@ -584,12 +611,22 @@ function patchLoader(jibo, skillsRoot) {
|
||||
}
|
||||
|
||||
log('Original menu config loaded successfully');
|
||||
log('Original button count:', config?.viewConfig?.list?.length || 0);
|
||||
try {
|
||||
const origCount = (config && config.viewConfig && config.viewConfig.list && config.viewConfig.list.length) ? config.viewConfig.list.length : 0;
|
||||
log('Original button count:', origCount);
|
||||
} catch (e) {
|
||||
log('Original button count: (unknown)');
|
||||
}
|
||||
|
||||
// Inject dynamic skills
|
||||
const patchedConfig = injectDynamicSkills(config);
|
||||
|
||||
log('Patched button count:', patchedConfig?.viewConfig?.list?.length || 0);
|
||||
try {
|
||||
const patchedCount = (patchedConfig && patchedConfig.viewConfig && patchedConfig.viewConfig.list && patchedConfig.viewConfig.list.length) ? patchedConfig.viewConfig.list.length : 0;
|
||||
log('Patched button count:', patchedCount);
|
||||
} catch (e) {
|
||||
log('Patched button count: (unknown)');
|
||||
}
|
||||
|
||||
if (callback) callback(null, patchedConfig);
|
||||
});
|
||||
@@ -608,6 +645,8 @@ function patchLoader(jibo, skillsRoot) {
|
||||
*/
|
||||
function clearCache() {
|
||||
_cachedMenuEntries = null;
|
||||
_cachedSkillsRoot = null;
|
||||
_cachedProvidersDir = null;
|
||||
_submenuConfigs = {};
|
||||
_dynamicSkillIds.clear();
|
||||
}
|
||||
|
||||
72
V3.1/build/opt/jibo/Jibo/Skills/@be/be/menu/menu-entries.js
Normal file
@@ -0,0 +1,72 @@
|
||||
"use strict";
|
||||
|
||||
const fs = require("fs");
|
||||
const path = require("path");
|
||||
|
||||
const skillsRoot = require("./skills-root");
|
||||
const scanner = require("./menu-entry-scanner");
|
||||
const providers = require("./menu-providers");
|
||||
|
||||
function isDirectory(p) {
|
||||
try {
|
||||
return fs.statSync(p).isDirectory();
|
||||
} catch (e) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
function expandSubmenuChildren(entries, root, opts) {
|
||||
(entries || []).forEach(function (e) {
|
||||
if (!e || e.type !== "submenu") return;
|
||||
|
||||
if (e.children && e.children.length) return;
|
||||
if (!e.childrenDir) return;
|
||||
|
||||
let dir = e.childrenDir;
|
||||
if (typeof dir !== "string" || dir.trim().length === 0) return;
|
||||
dir = dir.trim();
|
||||
|
||||
const resolved = (dir.charAt(0) === "/") ? dir : path.join(root, dir);
|
||||
if (!isDirectory(resolved)) {
|
||||
if (opts && opts.log) opts.log("submenu childrenDir not a directory", e.id, resolved);
|
||||
e.children = [];
|
||||
return;
|
||||
}
|
||||
|
||||
if (opts && opts.log) opts.log("submenu childrenDir scan", e.id, resolved);
|
||||
e.children = scanner.scanSubmenuChildren(resolved, { log: opts && opts.log, defaultIcon: opts && opts.defaultIcon, defaultColor: opts && opts.defaultColor, defaultOrder: opts && opts.defaultOrder });
|
||||
});
|
||||
}
|
||||
|
||||
function getMenuEntries(opts) {
|
||||
opts = opts || {};
|
||||
const root = skillsRoot.resolveSkillsRoot(opts.skillsRoot);
|
||||
const providerDir = opts.providersDir || skillsRoot.providersDirForRoot(root);
|
||||
|
||||
const scanned = scanner.scanAllMenuEntries(root, {
|
||||
log: opts.log,
|
||||
defaultIcon: opts.defaultIcon,
|
||||
defaultColor: opts.defaultColor,
|
||||
defaultOrder: opts.defaultOrder
|
||||
});
|
||||
|
||||
const provided = providers.loadProviderEntries(providerDir, { log: opts.log, skillsRoot: root, providersDir: providerDir });
|
||||
const merged = providers.mergeById(scanned, provided);
|
||||
|
||||
// Allow provider-defined submenus to be backed by an arbitrary directory.
|
||||
// Example: { type: "submenu", id: "fun", childrenDir: "FunStuff" }
|
||||
expandSubmenuChildren(merged, root, opts);
|
||||
|
||||
const dynamicIds = scanner.collectDynamicSkillIds(merged);
|
||||
|
||||
return {
|
||||
skillsRoot: root,
|
||||
providersDir: providerDir,
|
||||
entries: merged,
|
||||
dynamicSkillIds: dynamicIds
|
||||
};
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
getMenuEntries: getMenuEntries
|
||||
};
|
||||
@@ -0,0 +1,150 @@
|
||||
"use strict";
|
||||
|
||||
const fs = require("fs");
|
||||
const path = require("path");
|
||||
|
||||
function safeReadJson(filePath, logFn) {
|
||||
try {
|
||||
const txt = fs.readFileSync(filePath, "utf8");
|
||||
if (!txt || txt.trim().length === 0) return null;
|
||||
return JSON.parse(txt);
|
||||
} catch (e) {
|
||||
if (logFn) logFn("safeReadJson failed", filePath, e && e.message ? e.message : e);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
function isDirectory(p) {
|
||||
try {
|
||||
return fs.statSync(p).isDirectory();
|
||||
} catch (e) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
function scanSkillEntry(skillDir, folderName, opts) {
|
||||
opts = opts || {};
|
||||
const menuJsonPath = path.join(skillDir, "menuEntry.json");
|
||||
const menuJson = safeReadJson(menuJsonPath, opts.log);
|
||||
|
||||
if (!menuJson) return null;
|
||||
if (menuJson.hidden === true) return null;
|
||||
|
||||
const entry = {
|
||||
id: folderName,
|
||||
type: menuJson.type || "skill",
|
||||
title: menuJson.title || menuJson.label || folderName,
|
||||
icon: menuJson.icon || menuJson.iconSrc || (opts.defaultIcon || "resources/icons/settings.png"),
|
||||
color: menuJson.color || menuJson.colors || (opts.defaultColor || "default"),
|
||||
description: menuJson.description || "",
|
||||
path: skillDir,
|
||||
order: typeof menuJson.order === "number" ? menuJson.order : (typeof opts.defaultOrder === "number" ? opts.defaultOrder : 100),
|
||||
skillId: menuJson.skillId || folderName,
|
||||
submenuTitle: menuJson.submenuTitle || menuJson.title || folderName
|
||||
};
|
||||
|
||||
if (entry.type === "submenu") {
|
||||
entry.children = scanSubmenuChildren(skillDir, opts);
|
||||
}
|
||||
|
||||
// Legacy support
|
||||
if (menuJson.isSubmenu === true && entry.type !== "submenu") {
|
||||
entry.type = "submenu";
|
||||
entry.children = scanSubmenuChildren(skillDir, opts);
|
||||
}
|
||||
|
||||
return entry;
|
||||
}
|
||||
|
||||
function scanSubmenuChildren(submenuDir, opts) {
|
||||
const children = [];
|
||||
let items;
|
||||
try {
|
||||
items = fs.readdirSync(submenuDir);
|
||||
} catch (e) {
|
||||
return children;
|
||||
}
|
||||
|
||||
items.forEach(function (name) {
|
||||
if (!name || name.charAt(0) === ".") return;
|
||||
if (name === "@be" || name === "node_modules") return;
|
||||
|
||||
const childPath = path.join(submenuDir, name);
|
||||
if (!isDirectory(childPath)) return;
|
||||
|
||||
const entry = scanSkillEntry(childPath, name, opts);
|
||||
if (entry && entry.type === "skill") {
|
||||
children.push(entry);
|
||||
}
|
||||
});
|
||||
|
||||
children.sort(function (a, b) {
|
||||
if (a.order !== b.order) return a.order - b.order;
|
||||
return String(a.title || "").localeCompare(String(b.title || ""));
|
||||
});
|
||||
|
||||
return children;
|
||||
}
|
||||
|
||||
function scanAllMenuEntries(skillsRoot, opts) {
|
||||
opts = opts || {};
|
||||
const entries = [];
|
||||
|
||||
if (!skillsRoot || !fs.existsSync(skillsRoot)) {
|
||||
if (opts.log) opts.log("skills root missing", skillsRoot);
|
||||
return entries;
|
||||
}
|
||||
|
||||
let children;
|
||||
try {
|
||||
children = fs.readdirSync(skillsRoot);
|
||||
} catch (e) {
|
||||
if (opts.log) opts.log("failed to read skills root", skillsRoot, e && e.message ? e.message : e);
|
||||
return entries;
|
||||
}
|
||||
|
||||
children.forEach(function (name) {
|
||||
if (!name || name.charAt(0) === ".") return;
|
||||
if (name === "@be" || name === "node_modules") return;
|
||||
|
||||
const skillDir = path.join(skillsRoot, name);
|
||||
if (!isDirectory(skillDir)) return;
|
||||
|
||||
const entry = scanSkillEntry(skillDir, name, opts);
|
||||
if (entry) entries.push(entry);
|
||||
});
|
||||
|
||||
entries.sort(function (a, b) {
|
||||
if (a.order !== b.order) return a.order - b.order;
|
||||
return String(a.title || "").localeCompare(String(b.title || ""));
|
||||
});
|
||||
|
||||
return entries;
|
||||
}
|
||||
|
||||
function collectDynamicSkillIds(entries) {
|
||||
const ids = new Set();
|
||||
|
||||
(entries || []).forEach(function (e) {
|
||||
if (!e) return;
|
||||
if (e.type === "skill") {
|
||||
if (e.skillId) ids.add(e.skillId);
|
||||
if (e.id) ids.add(e.id);
|
||||
}
|
||||
if (e.type === "submenu" && e.children && e.children.length) {
|
||||
e.children.forEach(function (c) {
|
||||
if (!c) return;
|
||||
if (c.skillId) ids.add(c.skillId);
|
||||
if (c.id) ids.add(c.id);
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
return ids;
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
scanAllMenuEntries: scanAllMenuEntries,
|
||||
scanSubmenuChildren: scanSubmenuChildren,
|
||||
collectDynamicSkillIds: collectDynamicSkillIds
|
||||
};
|
||||
115
V3.1/build/opt/jibo/Jibo/Skills/@be/be/menu/menu-providers.js
Normal file
@@ -0,0 +1,115 @@
|
||||
"use strict";
|
||||
|
||||
const fs = require("fs");
|
||||
const path = require("path");
|
||||
|
||||
function isDirectory(p) {
|
||||
try {
|
||||
return fs.statSync(p).isDirectory();
|
||||
} catch (e) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
function safeReadJson(filePath, logFn) {
|
||||
try {
|
||||
const txt = fs.readFileSync(filePath, "utf8");
|
||||
if (!txt || txt.trim().length === 0) return null;
|
||||
return JSON.parse(txt);
|
||||
} catch (e) {
|
||||
if (logFn) logFn("provider json read failed", filePath, e && e.message ? e.message : e);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
function loadFromJson(filePath, ctx) {
|
||||
const obj = safeReadJson(filePath, ctx && ctx.log);
|
||||
if (!obj) return [];
|
||||
|
||||
if (Array.isArray(obj)) return obj;
|
||||
if (obj && Array.isArray(obj.entries)) return obj.entries;
|
||||
if (obj && Array.isArray(obj.buttons)) return obj.buttons;
|
||||
return [];
|
||||
}
|
||||
|
||||
function loadFromJs(filePath, ctx) {
|
||||
try {
|
||||
// eslint-disable-next-line global-require, import/no-dynamic-require
|
||||
const mod = require(filePath);
|
||||
if (!mod) return [];
|
||||
if (typeof mod === "function") return mod(ctx) || [];
|
||||
if (typeof mod.getEntries === "function") return mod.getEntries(ctx) || [];
|
||||
if (Array.isArray(mod.entries)) return mod.entries;
|
||||
return [];
|
||||
} catch (e) {
|
||||
if (ctx && ctx.log) ctx.log("provider js load failed", filePath, e && e.message ? e.message : e);
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
function loadProviderEntries(providersDir, ctx) {
|
||||
const out = [];
|
||||
|
||||
if (!providersDir || !fs.existsSync(providersDir) || !isDirectory(providersDir)) {
|
||||
return out;
|
||||
}
|
||||
|
||||
let files;
|
||||
try {
|
||||
files = fs.readdirSync(providersDir);
|
||||
} catch (e) {
|
||||
return out;
|
||||
}
|
||||
|
||||
files.sort();
|
||||
|
||||
files.forEach(function (name) {
|
||||
if (!name || name.charAt(0) === ".") return;
|
||||
const full = path.join(providersDir, name);
|
||||
if (isDirectory(full)) return;
|
||||
|
||||
if (name.indexOf(".json", name.length - 5) !== -1) {
|
||||
const entries = loadFromJson(full, ctx);
|
||||
entries.forEach(function (e) { out.push(e); });
|
||||
return;
|
||||
}
|
||||
|
||||
if (name.indexOf(".js", name.length - 3) !== -1) {
|
||||
const entries = loadFromJs(full, ctx);
|
||||
entries.forEach(function (e) { out.push(e); });
|
||||
}
|
||||
});
|
||||
|
||||
return out;
|
||||
}
|
||||
|
||||
// Merge provider entries into scanned entries by id.
|
||||
// Provider entries win on conflicts.
|
||||
function mergeById(scannedEntries, providerEntries) {
|
||||
const byId = {};
|
||||
|
||||
(scannedEntries || []).forEach(function (e) {
|
||||
if (!e || !e.id) return;
|
||||
byId[e.id] = e;
|
||||
});
|
||||
|
||||
(providerEntries || []).forEach(function (p) {
|
||||
if (!p || !p.id) return;
|
||||
byId[p.id] = p;
|
||||
});
|
||||
|
||||
const merged = Object.keys(byId).map(function (k) { return byId[k]; });
|
||||
merged.sort(function (a, b) {
|
||||
const ao = typeof a.order === "number" ? a.order : 100;
|
||||
const bo = typeof b.order === "number" ? b.order : 100;
|
||||
if (ao !== bo) return ao - bo;
|
||||
return String(a.title || a.label || a.id).localeCompare(String(b.title || b.label || b.id));
|
||||
});
|
||||
|
||||
return merged;
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
loadProviderEntries: loadProviderEntries,
|
||||
mergeById: mergeById
|
||||
};
|
||||
31
V3.1/build/opt/jibo/Jibo/Skills/@be/be/menu/skills-root.js
Normal file
@@ -0,0 +1,31 @@
|
||||
"use strict";
|
||||
|
||||
const path = require("path");
|
||||
const fs = require("fs");
|
||||
|
||||
const DEFAULT_ROOT = "/opt/jibo/Jibo/Skills";
|
||||
|
||||
function resolveSkillsRoot(overrideRoot) {
|
||||
if (overrideRoot && typeof overrideRoot === "string") return overrideRoot;
|
||||
if (process && process.env && process.env.JIBO_SKILLS_ROOT) return process.env.JIBO_SKILLS_ROOT;
|
||||
return DEFAULT_ROOT;
|
||||
}
|
||||
|
||||
function providersDirForRoot(skillsRoot) {
|
||||
// Keep providers inside Skills so they can be synced easily.
|
||||
// Default: /opt/jibo/Jibo/Skills/@be/menu-entries.d
|
||||
// Legacy fallback: /opt/jibo/Jibo/Skills/@be/menu-providers.d
|
||||
const v2 = path.join(skillsRoot, "@be", "menu-entries.d");
|
||||
const v1 = path.join(skillsRoot, "@be", "menu-providers.d");
|
||||
try {
|
||||
if (fs.existsSync(v2)) return v2;
|
||||
if (fs.existsSync(v1)) return v1;
|
||||
} catch (e) { /* ignore */ }
|
||||
return v2;
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
DEFAULT_ROOT: DEFAULT_ROOT,
|
||||
resolveSkillsRoot: resolveSkillsRoot,
|
||||
providersDirForRoot: providersDirForRoot
|
||||
};
|
||||
78
V3.1/build/opt/jibo/Jibo/Skills/@be/be/node_modules/@be/radio/node_modules/jibo-radio/LocalRadioPlayer.js
generated
vendored
Normal file
@@ -0,0 +1,78 @@
|
||||
const EventEmitter = require('events');
|
||||
|
||||
class LocalRadioPlayer extends EventEmitter {
|
||||
constructor() {
|
||||
super();
|
||||
// These are example streams. You will need to set up your own
|
||||
// Icecast/HTTP streams and replace these URLs with your server's IP.
|
||||
this._stations = {
|
||||
'Rock': 'http://192.168.1.100:8000/rock',
|
||||
'Pop': 'http://192.168.1.100:8000/pop',
|
||||
'Jazz': 'http://192.168.1.100:8000/jazz',
|
||||
'Classical': 'http://192.168.1.100:8000/classical',
|
||||
'Electronic': 'http://192.168.1.100:8000/electronic',
|
||||
'Hip-Hop': 'http://192.168.1.100:8000/hiphop'
|
||||
};
|
||||
this._audio = null;
|
||||
console.log("LocalRadioPlayer initialized");
|
||||
}
|
||||
|
||||
getStations(options) {
|
||||
console.log("LocalRadioPlayer: getStations called");
|
||||
const stationList = Object.keys(this._stations).map(genre => ({
|
||||
name: genre,
|
||||
id: genre
|
||||
}));
|
||||
return Promise.resolve(stationList);
|
||||
}
|
||||
|
||||
play(stationData) {
|
||||
console.log(`LocalRadioPlayer: play called with`, stationData);
|
||||
if (this._audio) {
|
||||
this._audio.pause();
|
||||
}
|
||||
const streamUrl = this._stations[stationData.id];
|
||||
if (!streamUrl) {
|
||||
const err = new Error(`Station ${stationData.id} not found.`);
|
||||
console.error("LocalRadioPlayer Error:", err);
|
||||
this.emit('error', err);
|
||||
return;
|
||||
}
|
||||
|
||||
console.log(`LocalRadioPlayer: Playing stream from ${streamUrl}`);
|
||||
this._audio = new Audio(streamUrl);
|
||||
this._audio.addEventListener('error', (e) => {
|
||||
console.error('LocalRadioPlayer Audio Error:', e);
|
||||
this.emit('error', new Error('Audio playback failed.'));
|
||||
});
|
||||
|
||||
this._audio.play()
|
||||
.then(() => {
|
||||
console.log("LocalRadioPlayer: Playback started.");
|
||||
this.emit('song-data', {
|
||||
title: `Streaming ${stationData.id}`,
|
||||
artist: 'Local Radio',
|
||||
albumArt: '' // No artwork for local streams
|
||||
});
|
||||
})
|
||||
.catch(err => {
|
||||
console.error("LocalRadioPlayer Playback Error:", err);
|
||||
this.emit('error', err);
|
||||
});
|
||||
}
|
||||
|
||||
stop() {
|
||||
console.log("LocalRadioPlayer: stop called");
|
||||
if (this._audio) {
|
||||
this._audio.pause();
|
||||
this._audio = null;
|
||||
}
|
||||
}
|
||||
|
||||
resizeArtwork(options) {
|
||||
// Not applicable for local streaming
|
||||
return Promise.resolve('');
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = LocalRadioPlayer;
|
||||
73
V3.1/build/opt/jibo/Jibo/Skills/@be/be/node_modules/jibo-radio/LocalRadioPlayer.js
generated
vendored
Normal file
@@ -0,0 +1,73 @@
|
||||
const EventEmitter = require('events');
|
||||
|
||||
class LocalRadioPlayer extends EventEmitter {
|
||||
constructor() {
|
||||
super();
|
||||
// These are example streams. You will need to set up your own
|
||||
// Icecast/HTTP streams and replace these URLs.
|
||||
this._stations = {
|
||||
'My Station': 'http://192.168.1.5:6767/Ninja%20Tuna.mp3'
|
||||
};
|
||||
this._audio = null;
|
||||
console.log("LocalRadioPlayer initialized");
|
||||
}
|
||||
|
||||
getStations(options) {
|
||||
console.log("LocalRadioPlayer: getStations called");
|
||||
const stationList = Object.keys(this._stations).map(genre => ({
|
||||
name: genre,
|
||||
id: genre
|
||||
}));
|
||||
return Promise.resolve(stationList);
|
||||
}
|
||||
|
||||
play(stationData) {
|
||||
console.log(`LocalRadioPlayer: play called with`, stationData);
|
||||
if (this._audio) {
|
||||
this._audio.pause();
|
||||
}
|
||||
const streamUrl = this._stations[stationData.id];
|
||||
if (!streamUrl) {
|
||||
const err = new Error(`Station ${stationData.id} not found.`);
|
||||
console.error("LocalRadioPlayer Error:", err);
|
||||
this.emit('error', err);
|
||||
return;
|
||||
}
|
||||
|
||||
console.log(`LocalRadioPlayer: Playing stream from ${streamUrl}`);
|
||||
this._audio = new Audio(streamUrl);
|
||||
this._audio.addEventListener('error', (e) => {
|
||||
console.error('LocalRadioPlayer Audio Error:', e);
|
||||
this.emit('error', new Error('Audio playback failed.'));
|
||||
});
|
||||
|
||||
this._audio.play()
|
||||
.then(() => {
|
||||
console.log("LocalRadioPlayer: Playback started.");
|
||||
this.emit('song-data', {
|
||||
title: `Streaming ${stationData.id}`,
|
||||
artist: 'Local Radio',
|
||||
albumArt: '' // No artwork for local streams
|
||||
});
|
||||
})
|
||||
.catch(err => {
|
||||
console.error("LocalRadioPlayer Playback Error:", err);
|
||||
this.emit('error', err);
|
||||
});
|
||||
}
|
||||
|
||||
stop() {
|
||||
console.log("LocalRadioPlayer: stop called");
|
||||
if (this._audio) {
|
||||
this._audio.pause();
|
||||
this._audio = null;
|
||||
}
|
||||
}
|
||||
|
||||
resizeArtwork(options) {
|
||||
// Not applicable for local streaming
|
||||
return Promise.resolve('');
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = LocalRadioPlayer;
|
||||
1028
V3.1/build/opt/jibo/Jibo/Skills/@be/be/node_modules/jibo-radio/lib/jibo-radio.js
generated
vendored
|
After Width: | Height: | Size: 79 KiB |
|
After Width: | Height: | Size: 173 KiB |
|
After Width: | Height: | Size: 62 KiB |
|
After Width: | Height: | Size: 177 KiB |
BIN
V3.1/build/opt/jibo/Jibo/Skills/@be/be/resources/JiboSplash.kra
Normal file
|
Before Width: | Height: | Size: 24 KiB After Width: | Height: | Size: 31 KiB |
|
Before Width: | Height: | Size: 14 KiB After Width: | Height: | Size: 24 KiB |
BIN
V3.1/build/opt/jibo/Jibo/Skills/@be/be/resources/jibo_logo.png
Normal file
|
After Width: | Height: | Size: 111 KiB |
BIN
V3.1/build/opt/jibo/Jibo/Skills/@be/be/resources/jibo_logo.png~
Normal file
|
After Width: | Height: | Size: 107 KiB |
BIN
V3.1/build/opt/jibo/Jibo/Skills/@be/be/resources/jibo_logo1.png
Normal file
|
After Width: | Height: | Size: 76 KiB |