made-bridge 0.0.0-stage → 0.2.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (57) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +45 -2
  3. package/builtin/plugins/README.md +22 -0
  4. package/builtin/themes/README.md +64 -0
  5. package/builtin/themes/sunset/theme.json +19 -0
  6. package/config.example.json +20 -0
  7. package/demo-project/README.md +5 -0
  8. package/demo-project/index.mjs +14 -0
  9. package/package.json +48 -4
  10. package/public/app.js +1028 -0
  11. package/public/assistants.html +67 -0
  12. package/public/bean-icon.png +0 -0
  13. package/public/i18n-messages.js +967 -0
  14. package/public/i18n-static.js +615 -0
  15. package/public/i18n.js +50 -0
  16. package/public/index.html +149 -0
  17. package/public/made-icon.png +0 -0
  18. package/public/plugins.html +64 -0
  19. package/public/plugins.js +335 -0
  20. package/public/provider-icon.js +77 -0
  21. package/public/styles.css +445 -0
  22. package/public/tasks.html +85 -0
  23. package/src/agent-icons.mjs +138 -0
  24. package/src/agents/codex.mjs +94 -0
  25. package/src/agents/cursor.mjs +31 -0
  26. package/src/agents/events.mjs +92 -0
  27. package/src/agents/index.mjs +7 -0
  28. package/src/agents/qoder.mjs +34 -0
  29. package/src/agents/shared.mjs +191 -0
  30. package/src/agents/workbuddy.mjs +121 -0
  31. package/src/board-link.mjs +266 -0
  32. package/src/config.mjs +196 -0
  33. package/src/doctor.mjs +146 -0
  34. package/src/i18n.mjs +257 -0
  35. package/src/installed.mjs +291 -0
  36. package/src/jobs.mjs +369 -0
  37. package/src/language-settings.mjs +38 -0
  38. package/src/model-settings.mjs +63 -0
  39. package/src/openai-key.mjs +47 -0
  40. package/src/oray.mjs +243 -0
  41. package/src/pairing.mjs +203 -0
  42. package/src/plugin-package.mjs +132 -0
  43. package/src/plugin-runtime.mjs +169 -0
  44. package/src/plugins/contracts.d.ts +184 -0
  45. package/src/plugins/enhance.mjs +156 -0
  46. package/src/plugins/registry.mjs +146 -0
  47. package/src/providers.mjs +75 -0
  48. package/src/server.mjs +950 -0
  49. package/src/sessions.mjs +134 -0
  50. package/src/speech/index.mjs +10 -0
  51. package/src/speech/openai.mjs +48 -0
  52. package/src/speech/whisper.mjs +59 -0
  53. package/src/speech-settings.mjs +209 -0
  54. package/src/transcribe.mjs +85 -0
  55. package/src/tunnel.mjs +531 -0
  56. package/src/usb-receiver.mjs +664 -0
  57. package/src/voice-settings.mjs +69 -0
@@ -0,0 +1,146 @@
1
+ import { pathToFileURL } from 'node:url';
2
+ import path from 'node:path';
3
+ import fs from 'node:fs';
4
+ import { packageRoots } from '../config.mjs';
5
+ import { isPackageEntry, packageDirectories, readPackageSettings } from '../plugin-package.mjs';
6
+ import { MODEL_ID } from '../model-settings.mjs';
7
+ import { builtinAgents } from '../agents/index.mjs';
8
+ import { normalizeAgentIcon } from '../agent-icons.mjs';
9
+ import { builtinSpeechRecognizers } from '../speech/index.mjs';
10
+
11
+ export const PLUGIN_API_VERSION = 1;
12
+ export const MAX_DEVICE_AGENTS = 12;
13
+ export const PLUGIN_ID = /^[a-z0-9][a-z0-9_-]{0,39}$/;
14
+ const reserved = new Set(['__proto__', 'constructor', 'prototype']);
15
+ const isRecord = (v) => v && typeof v === 'object' && !Array.isArray(v);
16
+ const labelOK = (v) => typeof v === 'string' && v.trim() && Buffer.byteLength(v) <= 64 && !/[\p{Cc}\p{Cf}]/u.test(v);
17
+ export function validatePlugin(plugin) {
18
+ const fail = (message) => { throw new Error(`插件 ${plugin?.id || '(未知)'}:${message}`); };
19
+ if (!isRecord(plugin) || plugin.apiVersion !== PLUGIN_API_VERSION) fail('不支持的 apiVersion,当前为 1');
20
+ if (!PLUGIN_ID.test(plugin.id) || reserved.has(plugin.id)) fail('ID 必须为 1–40 个小写字母、数字、下划线或连字符');
21
+ if (!labelOK(plugin.label)) fail('名称必须为不超过 64 字节的可见文字');
22
+ if (typeof plugin.probe !== 'function') fail('缺少同步 probe');
23
+ if (plugin.kind === 'coding-agent') {
24
+ const c = plugin.capabilities;
25
+ if (!isRecord(c) || !['native', 'external_unscoped'].includes(c.session) ||
26
+ ['model', 'cancel', 'progress'].some((k) => typeof c[k] !== 'boolean')) fail('capabilities 无效');
27
+ if (typeof plugin.run !== 'function') fail('缺少 run');
28
+ try { normalizeAgentIcon(plugin.icon); } catch (error) { fail(error.message); }
29
+ if (plugin.sessionIdPattern !== undefined && (!(plugin.sessionIdPattern instanceof RegExp) || plugin.sessionIdPattern.global || plugin.sessionIdPattern.sticky)) fail('sessionIdPattern 必须是不含 g/y 标志的正则表达式');
30
+ if (plugin.model && (!c.model || !isRecord(plugin.model) || typeof plugin.model.default !== 'string' || typeof plugin.model.required !== 'boolean')) fail('model 元数据无效');
31
+ if (plugin.model && ((!plugin.model.default && plugin.model.required) || (plugin.model.default && !MODEL_ID.test(plugin.model.default)))) fail('model 默认值无效');
32
+ for (const k of ['readUpdates', 'syncJobState']) if (plugin[k] !== undefined && typeof plugin[k] !== 'function') fail(`${k} 必须是函数`);
33
+ } else if (plugin.kind === 'board-plugin') {
34
+ const names = (key, min) => {
35
+ const list = plugin[key];
36
+ if (!Array.isArray(list) || list.length > 16 || list.length < min || list.some((name) => typeof name !== 'string' || !/^[a-z][a-z0-9_]{0,15}(?:\.[a-z][a-z0-9_]{0,15}){0,2}$/.test(name))) fail(`${key} 无效`);
37
+ if (new Set(list).size !== list.length) fail(`${key} 有重复项`);
38
+ };
39
+ names('events', 1);
40
+ names('commands', 0);
41
+ if (typeof plugin.onEvent !== 'function') fail('缺少 onEvent');
42
+ // 增强钩子是可选项:不声明就不参与管道,声明了就必须是函数。
43
+ for (const key of ['onTurnInput', 'onTurnOutput', 'onJob']) {
44
+ if (plugin[key] !== undefined && typeof plugin[key] !== 'function') fail(`${key} 必须是函数`);
45
+ }
46
+ } else if (plugin.kind === 'speech-recognizer') {
47
+ if (typeof plugin.transcribe !== 'function' || !Array.isArray(plugin.fields) || plugin.fields.length > 16) fail('缺少 transcribe 或 fields 无效');
48
+ const keys = new Set();
49
+ for (const f of plugin.fields) {
50
+ if (!isRecord(f) || !/^[A-Za-z][A-Za-z0-9_]{0,39}$/.test(f.key) || reserved.has(f.key) || keys.has(f.key) || !labelOK(f.label) || !['text', 'secret', 'url', 'model', 'path'].includes(f.type)) fail('配置字段无效或重复');
51
+ if (f.default !== undefined && (typeof f.default !== 'string' || f.default.length > 4096 || (f.type === 'secret' && f.default))) fail('字段默认值无效;密钥不能有默认值');
52
+ if (f.required !== undefined && typeof f.required !== 'boolean') fail('字段 required 必须是布尔值');
53
+ keys.add(f.key);
54
+ }
55
+ if (plugin.validateSettings !== undefined && typeof plugin.validateSettings !== 'function') fail('validateSettings 必须是函数');
56
+ if (plugin.presets !== undefined) {
57
+ if (!Array.isArray(plugin.presets) || plugin.presets.length > 16) fail('presets 无效');
58
+ const ids = new Set();
59
+ for (const p of plugin.presets) {
60
+ if (!isRecord(p) || !PLUGIN_ID.test(p.id) || ids.has(p.id) || !labelOK(p.label) || !isRecord(p.values)) fail('预设无效');
61
+ for (const [key, value] of Object.entries(p.values)) if (!keys.has(key) || typeof value !== 'string' || plugin.fields.find(f => f.key === key).type === 'secret') fail('预设不能包含密钥或未声明字段');
62
+ ids.add(p.id);
63
+ }
64
+ }
65
+ } else fail('kind 应为 coding-agent、board-plugin 或 speech-recognizer');
66
+ return plugin;
67
+ }
68
+
69
+ export function createPluginRegistry(plugins = [...builtinAgents, ...builtinSpeechRecognizers]) {
70
+ const entries = new Map();
71
+ for (const raw of plugins) {
72
+ const plugin = validatePlugin(raw);
73
+ if (entries.has(plugin.id)) throw new Error(`插件 ID 重复:${plugin.id}`);
74
+ entries.set(plugin.id, plugin.kind === 'coding-agent' ? Object.freeze({ ...plugin, icon: normalizeAgentIcon(plugin.icon) }) : plugin);
75
+ }
76
+ const agents = [...entries.values()].filter(p => p.kind === 'coding-agent');
77
+ const speech = [...entries.values()].filter(p => p.kind === 'speech-recognizer');
78
+ const boards = [...entries.values()].filter(p => p.kind === 'board-plugin');
79
+ if (agents.length > MAX_DEVICE_AGENTS) throw new Error(`码得最多支持 ${MAX_DEVICE_AGENTS} 个助手,请禁用多余插件`);
80
+ return Object.freeze({
81
+ agents: () => [...agents], agent: (id) => agents.find(p => p.id === id), hasAgent: (id) => agents.some(p => p.id === id),
82
+ speechRecognizers: () => [...speech], speechRecognizer: (id) => speech.find(p => p.id === id), hasSpeechRecognizer: (id) => speech.some(p => p.id === id),
83
+ boardPlugins: () => [...boards], boardPlugin: (id) => boards.find(p => p.id === id),
84
+ });
85
+ }
86
+
87
+ export async function loadConfiguredPlugins(config) {
88
+ const disabled = new Set(config.disabledPlugins || []);
89
+ for (const recognizer of builtinSpeechRecognizers) {
90
+ if (disabled.has(recognizer.id)) throw new Error(`语音识别 ${recognizer.id} 是内置接口,不能通过 disabledPlugins 禁用;请在语音设置中配置`);
91
+ }
92
+ const plugins = [...builtinAgents.filter(p => !disabled.has(p.id)), ...builtinSpeechRecognizers];
93
+ const loadFile = async (filename, label) => {
94
+ if (!/\.(mjs|js)$/.test(filename) || !fs.statSync(filename).isFile()) throw new Error(`插件模块无效:${label}`);
95
+ const exported = (await import(pathToFileURL(filename).href)).default;
96
+ const bundle = Array.isArray(exported) ? exported : [exported];
97
+ const pack = isPackageEntry(filename) ? readPackageSettings(path.dirname(filename)) : null;
98
+ for (const p of bundle) {
99
+ validatePlugin(p);
100
+ if (p.kind !== 'coding-agent' && p.kind !== 'board-plugin') throw new Error(`插件 ${p.id}:外部插件仅支持 coding-agent;语音识别请配置兼容接口的 URL、API Key 和模型名`);
101
+ if (pack) {
102
+ const mismatch = pack.id && pack.id !== p.id;
103
+ const error = mismatch ? '插件配置规范与插件 ID 不一致' : pack.error;
104
+ config.pluginPackages = { ...(config.pluginPackages || {}), [p.id]: {
105
+ name: path.basename(path.dirname(filename)),
106
+ fields: error ? [] : pack.fields,
107
+ values: error ? {} : pack.values,
108
+ error,
109
+ } };
110
+ if (!error) config.pluginSettings = { ...(config.pluginSettings || {}), [p.id]: pack.values };
111
+ }
112
+ if (!disabled.has(p.id)) plugins.push(p);
113
+ }
114
+ };
115
+ const explicitPaths = new Set();
116
+ for (const entry of config.plugins || []) {
117
+ if (typeof entry !== 'string' || !entry || /^[a-z]+:/i.test(entry)) throw new Error('plugins 仅接受显式的本地模块路径');
118
+ const filename = path.resolve(config.configDirectory || process.cwd(), entry);
119
+ if (!fs.existsSync(filename)) throw new Error(`插件模块无效:${entry}`);
120
+ explicitPaths.add(fs.realpathSync(filename));
121
+ await loadFile(filename, entry);
122
+ }
123
+ // 扫内置 bridge/builtin/plugins 与用户 ~/made/plugins:同名条目用户目录覆盖内置。
124
+ const scanned = new Map();
125
+ for (const root of packageRoots(config)) {
126
+ const pluginsDir = path.join(root, 'plugins');
127
+ if (!fs.existsSync(pluginsDir) || !fs.statSync(pluginsDir).isDirectory()) continue;
128
+ for (const item of packageDirectories(pluginsDir)) {
129
+ scanned.set(item.name, { filename: item.entry, label: `./plugins/${item.name}/${path.basename(item.entry)}` });
130
+ }
131
+ for (const name of fs.readdirSync(pluginsDir).sort()) {
132
+ if (!/\.(mjs|js)$/.test(name)) continue;
133
+ const filename = path.join(pluginsDir, name);
134
+ if (!fs.statSync(filename).isFile()) continue;
135
+ scanned.set(name, { filename, label: `./plugins/${name}` });
136
+ }
137
+ }
138
+ for (const { filename, label } of scanned.values()) {
139
+ if (explicitPaths.has(fs.realpathSync(filename))) continue;
140
+ explicitPaths.add(fs.realpathSync(filename));
141
+ await loadFile(filename, label);
142
+ }
143
+ const registry = createPluginRegistry(plugins);
144
+ if (!registry.hasAgent(config.defaultProvider)) throw new Error('默认编程助手未安装或已禁用');
145
+ return registry;
146
+ }
@@ -0,0 +1,75 @@
1
+ import { builtinAgents, builtinAgent } from './agents/index.mjs';
2
+ import { normalizeAgentIcon } from './agent-icons.mjs';
3
+ import { translateKnownMessage } from './i18n.mjs';
4
+
5
+ export { builtinAgents } from './agents/index.mjs';
6
+
7
+ export function agentFor(config, id) {
8
+ return config.pluginsRuntime ? config.pluginsRuntime.agent(id) : builtinAgent(id);
9
+ }
10
+
11
+ export function isBuiltinAgent(plugin) {
12
+ const builtin = builtinAgent(plugin?.id);
13
+ return Boolean(builtin && plugin.run === builtin.run && plugin.probe === builtin.probe);
14
+ }
15
+
16
+ export function providerStates(config, locale = config.locale) {
17
+ const agents = config.pluginsRuntime ? config.pluginsRuntime.agents() : builtinAgents;
18
+ return agents.map((plugin) => {
19
+ let state;
20
+ let bridgeReason = isBuiltinAgent(plugin);
21
+ try {
22
+ state = plugin.probe(config);
23
+ if (state?.then) {
24
+ // A mistaken async probe cannot hold up /api/state or produce an
25
+ // unhandled rejection. The plugin contract requires a sync result.
26
+ Promise.resolve(state).catch(() => {});
27
+ bridgeReason = true;
28
+ state = { available: false, reason: '插件状态检查必须同步返回' };
29
+ }
30
+ } catch { bridgeReason = true; state = { available: false, reason: `${plugin.label} 状态检查失败` }; }
31
+ return {
32
+ id: plugin.id, label: plugin.label, icon: normalizeAgentIcon(plugin.icon),
33
+ available: state?.available === true,
34
+ reason: typeof state?.reason === 'string' ? (bridgeReason ? translateKnownMessage(state.reason, locale) : state.reason).slice(0, 512) : '',
35
+ mode: typeof state?.mode === 'string' ? state.mode.slice(0, 64) : 'plugin',
36
+ capabilities: {
37
+ session: plugin.capabilities.session, model: plugin.capabilities.model,
38
+ cancel: plugin.capabilities.cancel, progress: plugin.capabilities.progress,
39
+ },
40
+ model: plugin.capabilities.model && plugin.model
41
+ ? { default: plugin.model.default, required: plugin.model.required } : null,
42
+ canCancelRunning: plugin.capabilities.cancel,
43
+ };
44
+ });
45
+ }
46
+
47
+ // Compatibility for existing stream tests and embedders. Actual parsing belongs
48
+ // to each plugin and adding an agent never adds another branch here.
49
+ export function createCliEventStream(provider, onProgress, onSession) {
50
+ const plugin = builtinAgent(provider);
51
+ if (!plugin?.createEvents) throw new Error(`编程工具不支持 CLI 事件:${provider}`);
52
+ return plugin.createEvents(onProgress, onSession);
53
+ }
54
+
55
+ export function createRunner(config) {
56
+ const runner = async (job, project, signal, onProgress = () => {}, onSession = () => {}) => {
57
+ const plugin = agentFor(config, job.provider);
58
+ if (!plugin) throw new Error('编程工具插件未加载,请先安装或启用对应插件');
59
+ if (signal.aborted) throw new Error('任务已取消');
60
+ // Capture the model when starting the turn; editing desktop settings does
61
+ // not mutate an in-flight run. Model-less adapters never receive a model.
62
+ const model = plugin.capabilities.model ? config.models?.[plugin.id] : undefined;
63
+ return plugin.run({
64
+ job, project, signal, config, model,
65
+ onProgress: plugin.capabilities.progress ? onProgress : () => {},
66
+ onSession: plugin.capabilities.session === 'native' ? onSession : () => {},
67
+ });
68
+ };
69
+ runner.agent = (id) => agentFor(config, id);
70
+ runner.hasAgent = (id) => Boolean(runner.agent(id));
71
+ runner.canReadUpdates = (job) => typeof runner.agent(job.provider)?.readUpdates === 'function';
72
+ runner.readUpdates = (job) => runner.agent(job.provider)?.readUpdates?.({ job, config });
73
+ runner.syncJobState = (job) => runner.agent(job.provider)?.syncJobState?.(job);
74
+ return runner;
75
+ }