@ninkch/dsh-infinite-canvas 0.17.1 → 0.17.3

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.
package/index.js CHANGED
@@ -1,215 +1,337 @@
1
- /**
2
- * Host half of the infinite-canvas bundle.
3
- *
4
- * Registers one JSON-RPC style route (`/infinite-canvas/api/rpc`) the browser
5
- * half calls for AI text generation (剧本 / 分镜提示词). The completion runs
6
- * through the Harness `llm` Service — the same providers/models the user has
7
- * configured for the Harness itself, no extra API key handling here.
8
- */
9
- import { spawn } from 'node:child_process';
10
- import fs from 'node:fs/promises';
11
- import path from 'node:path';
12
- import { fileURLToPath } from 'node:url';
13
-
14
- const NAME = 'infinite-canvas';
15
- const VERSION = '0.17.0';
16
- const HERE = path.dirname(fileURLToPath(import.meta.url));
17
-
18
- /** 运行 git 子命令(cwd = 插件目录 = 仓库根)。 */
19
- function runGit(args, timeoutMs = 90000) {
20
- return new Promise((resolve) => {
21
- const started = Date.now();
22
- const stdout = [];
23
- const stderr = [];
24
- let child;
25
- try {
26
- child = spawn('git', args.map(String), { cwd: HERE, windowsHide: true });
27
- } catch (e) {
28
- resolve({ ok: false, stdout: '', stderr: String(e?.message ?? e) });
29
- return;
30
- }
31
- const timer = setTimeout(() => { try { child.kill(); } catch { /* ignore */ } }, Math.max(5000, timeoutMs));
32
- child.stdout?.setEncoding('utf8');
33
- child.stderr?.setEncoding('utf8');
34
- child.stdout?.on('data', (d) => stdout.push(String(d)));
35
- child.stderr?.on('data', (d) => stderr.push(String(d)));
36
- child.on('error', (e) => { clearTimeout(timer); resolve({ ok: false, stdout: stdout.join(''), stderr: String(e?.message ?? e) }); });
37
- child.on('close', (code) => {
38
- clearTimeout(timer);
39
- resolve({ ok: code === 0, code, stdout: stdout.join(''), stderr: stderr.join(''), ms: Date.now() - started });
40
- });
41
- });
42
- }
43
-
44
- /** 更新检查:fetch origin + 对比本地/远端版本。 */
45
- async function updateCheck() {
46
- const fetchRes = await runGit(['fetch', 'origin', '--quiet'], 60000);
47
- if (!fetchRes.ok) throw new Error('git fetch 失败:' + (fetchRes.stderr || '').slice(0, 300));
48
- const head = (await runGit(['rev-parse', '--short', 'HEAD'])).stdout.trim();
49
- const remoteHead = (await runGit(['rev-parse', '--short', 'origin/main'])).stdout.trim();
50
- const behind = Number((await runGit(['rev-list', '--count', 'HEAD..origin/main'])).stdout.trim()) || 0;
51
- let remoteVersion = null;
52
- try {
53
- const pj = await runGit(['show', 'origin/main:package.json']);
54
- remoteVersion = JSON.parse(pj.stdout).version ?? null;
55
- } catch { /* 远端无 package.json 时忽略 */ }
56
- let runningVersion = null;
57
- try { runningVersion = JSON.parse(await fs.readFile(path.join(HERE, 'package.json'), 'utf8')).version ?? null; } catch { /* 忽略 */ }
58
- return { head, remoteHead, behind, remoteVersion, runningVersion, upToDate: head === remoteHead };
59
- }
60
-
61
- /** 应用更新:git pull --ff-only;报告 index.js 是否变更(决定是否需要重启 Harness)。 */
62
- async function updateApply() {
63
- const before = (await runGit(['rev-parse', 'HEAD:index.js'])).stdout.trim();
64
- const pull = await runGit(['pull', '--ff-only', 'origin', 'main'], 180000);
65
- const after = (await runGit(['rev-parse', 'HEAD:index.js'])).stdout.trim();
66
- let version = null;
67
- try { version = JSON.parse(await fs.readFile(path.join(HERE, 'package.json'), 'utf8')).version ?? null; } catch { /* 忽略 */ }
68
- return {
69
- ok: pull.ok,
70
- output: ((pull.stdout || '') + '\n' + (pull.stderr || '')).trim().slice(-1500),
71
- hostChanged: pull.ok && before !== after,
72
- version,
73
- };
74
- }
75
-
76
- export const inject = ['webServer'];
77
-
78
- function resolveModel(providers, want) {
79
- const byProvider = want && want.provider
80
- ? providers.find((p) => p.id === want.provider)
81
- : null;
82
- const provider = byProvider || providers.find((p) => (p.models ?? []).length) || null;
83
- if (!provider) return null;
84
- const byModel = want && want.model
85
- ? provider.models.find((m) => m.id === want.model)
86
- : null;
87
- const model = byModel || provider.models[0] || null;
88
- if (!model) return null;
89
- return { provider: provider.id, model: model.id };
90
- }
91
-
92
- export function apply(ctx) {
93
- ctx.effect(() => {
94
- const llm = () => ctx.get('llm');
95
-
96
- async function listLlmOptions() {
97
- const service = llm();
98
- if (!service) return { providers: [], resolved: null, error: 'llm 服务不可用' };
99
- const out = [];
100
- const seen = new Set();
101
- const pushProvider = async (id, name) => {
102
- if (!id || seen.has(id)) return;
103
- seen.add(id);
104
- try {
105
- const models = await service.listModels(id);
106
- out.push({
107
- id,
108
- name: name ?? id,
109
- models: (models ?? []).map((m) => ({ id: m.id, name: m.name ?? m.id })),
110
- });
111
- } catch (e) {
112
- out.push({ id, name: name ?? id, models: [], error: String((e && e.message) || e) });
113
- }
114
- };
115
- try {
116
- for (const p of (service.listProviders() ?? [])) await pushProvider(p.id, p.name ?? p.id);
117
- } catch { /* 继续尝试 configurable providers */ }
118
- try {
119
- if (typeof service.listConfigurableProviders === 'function') {
120
- for (const c of (service.listConfigurableProviders() ?? [])) {
121
- const id = c && (c.id ?? c.key ?? c.settingsNs ?? c.name);
122
- if (id) await pushProvider(String(id), (c.name ?? c.title) ?? String(id));
123
- }
124
- }
125
- } catch { /* ignore */ }
126
- return { providers: out, resolved: resolveModel(out) };
127
- }
128
-
129
- async function complete(args) {
130
- const service = llm();
131
- if (!service) throw new Error('llm 服务不可用');
132
- const want = args ?? {};
133
- const { providers } = await listLlmOptions();
134
- const resolved = resolveModel(providers, want);
135
- if (!resolved) throw new Error('没有可用模型,请先在 Harness 设置中配置模型提供方');
136
- let text = '';
137
- const stream = service.stream({
138
- provider: resolved.provider,
139
- model: resolved.model,
140
- messages: [
141
- { role: 'system', content: [{ type: 'text', text: String(want.system ?? '') }] },
142
- { role: 'user', content: [{ type: 'text', text: String(want.user ?? '') }] },
143
- ],
144
- temperature: typeof want.temperature === 'number' ? want.temperature : 0.7,
145
- maxTokens: typeof want.maxTokens === 'number' ? want.maxTokens : 4000,
146
- });
147
- for await (const chunk of stream) {
148
- if (chunk.type === 'text-delta') text += chunk.text;
149
- }
150
- return { text, provider: resolved.provider, model: resolved.model };
151
- }
152
-
153
- const methods = {
154
- async ping() { return { ok: true, version: VERSION }; },
155
- async llmOptions() { return listLlmOptions(); },
156
- async llmComplete(args) { return complete(args); },
157
- async updateCheck() { return updateCheck(); },
158
- async updateApply() { return updateApply(); },
159
- async listSkills() {
160
- const skills = ctx.get('skills');
161
- if (!skills) return { skills: [], error: 'skills 服务不可用' };
162
- try {
163
- const list = await skills.list({ limit: 200 });
164
- return {
165
- skills: (list ?? []).map((s) => ({ name: s.name, description: s.description ?? '' })),
166
- };
167
- } catch (e) {
168
- return { skills: [], error: String((e && e.message) || e) };
169
- }
170
- },
171
- async readSkill(args) {
172
- const skills = ctx.get('skills');
173
- if (!skills) throw new Error('skills 服务不可用');
174
- const name = String((args && args.name) ?? '');
175
- if (!name) throw new Error('缺少技能名');
176
- const def = await skills.get(name);
177
- if (!def) throw new Error('技能不存在: ' + name);
178
- const text = typeof def === 'string'
179
- ? def
180
- : (def.content ?? def.body ?? def.instructions ?? def.markdown ?? null);
181
- return {
182
- name,
183
- text: String(text ?? ((def && typeof def === 'object') ? JSON.stringify(def) : '')).slice(0, 80000),
184
- };
185
- },
186
- };
187
-
188
- return ctx.webServer.register({
189
- kind: 'exact',
190
- path: `/${NAME}/api/rpc`,
191
- async handler(req, res) {
192
- res.setHeader('content-type', 'application/json; charset=utf-8');
193
- try {
194
- if (req.method !== 'POST') {
195
- res.statusCode = 405;
196
- res.end(JSON.stringify({ error: '仅接受 POST' }));
197
- return;
198
- }
199
- let raw = '';
200
- for await (const chunk of req) {
201
- raw += chunk;
202
- if (raw.length > 2e6) throw new Error('请求过大');
203
- }
204
- const { method, args } = JSON.parse(raw);
205
- const fn = methods[method];
206
- if (!fn) throw new Error('未知操作: ' + method);
207
- res.end(JSON.stringify(await fn(args ?? {})));
208
- } catch (e) {
209
- res.statusCode = 400;
210
- res.end(JSON.stringify({ error: String((e && e.message) || e) }));
211
- }
212
- },
213
- });
214
- }, `${NAME}: api route`);
215
- }
1
+ /**
2
+ * Host half of the infinite-canvas bundle.
3
+ *
4
+ * Registers one JSON-RPC style route (`/infinite-canvas/api/rpc`) the browser
5
+ * half calls for AI text generation (剧本 / 分镜提示词). The completion runs
6
+ * through the Harness `llm` Service — the same providers/models the user has
7
+ * configured for the Harness itself, no extra API key handling here.
8
+ */
9
+ import { spawn } from 'node:child_process';
10
+ import fs from 'node:fs/promises';
11
+ import { readFileSync, existsSync, readdirSync } from 'node:fs';
12
+ import os from 'node:os';
13
+ import path from 'node:path';
14
+ import { fileURLToPath } from 'node:url';
15
+
16
+ const NAME = 'infinite-canvas';
17
+ const HERE = path.dirname(fileURLToPath(import.meta.url));
18
+
19
+ // 版本单一来源 = 当前副本的 package.json(硬编码常量曾与包版本漂移)
20
+ let VERSION = '0.0.0';
21
+ try {
22
+ VERSION = JSON.parse(readFileSync(path.join(HERE, 'package.json'), 'utf8')).version || VERSION;
23
+ } catch { /* 保留默认值 */ }
24
+
25
+ // 更新检查的唯一信息源 = npm registry latest(与安装方式无关:
26
+ // npm 在线安装 / 本地 link 开发态 / 其它方式,一律走同一条逻辑)。
27
+ const NPM_LATEST_URL = 'https://registry.npmjs.org/' + encodeURIComponent('@ninkch/dsh-infinite-canvas') + '/latest';
28
+ const PKG_NAME = '@ninkch/dsh-infinite-canvas';
29
+
30
+ // ---------- 安装形态探测(用于"点击即更新") ----------
31
+ // 受管安装 = 本副本位于某个 DSH profile 的 node_modules 下(向上找带 dsh.profile.bundles 的 package.json)。
32
+ // 开发态(目录含 .git)不受管 → 点击更新时降级为纯引导。
33
+ const PROFILE_DIR = (() => {
34
+ if (existsSync(path.join(HERE, '.git'))) return null;
35
+ let dir = HERE;
36
+ for (let i = 0; i < 6; i++) {
37
+ dir = path.dirname(dir);
38
+ try {
39
+ const m = JSON.parse(readFileSync(path.join(dir, 'package.json'), 'utf8'));
40
+ if (m && m.dsh && m.dsh.profile && Array.isArray(m.dsh.profile.bundles)) return dir;
41
+ } catch { /* 继续向上 */ }
42
+ }
43
+ return null;
44
+ })();
45
+
46
+ /** 定位 pnpm:Desktop 安装布局 → 用户运行时目录 → PATH。返回 { execPath, args 前缀 } 或 null。 */
47
+ function findPnpm() {
48
+ const candidates = [];
49
+ try {
50
+ candidates.push(path.join(path.dirname(process.execPath), 'resources', 'runtime', 'pnpm', 'bin', 'pnpm.mjs'));
51
+ } catch { /* execPath 不可用 */ }
52
+ try {
53
+ const runtimes = path.join(os.homedir(), '.dsh', 'dsh-runtimes');
54
+ for (const entry of readdirSync(runtimes)) {
55
+ candidates.push(path.join(runtimes, entry, 'dependencies', 'pnpm', 'bin', 'pnpm.mjs'));
56
+ }
57
+ } catch { /* 无运行时目录 */ }
58
+ for (const c of candidates) {
59
+ try { if (existsSync(c)) return { kind: 'script', node: process.execPath, script: c }; } catch { /* 跳过 */ }
60
+ }
61
+ try { if (existsSync('C:\\Program Files\\nodejs\\pnpm.cmd')) return { kind: 'path', command: 'pnpm' }; } catch { /* 跳过 */ }
62
+ return { kind: 'path', command: 'pnpm' }; // 交给 spawn 兜底(PATH 上有就用)
63
+ }
64
+
65
+ /** 在 profile 目录执行 pnpm add <spec> --save-exact。opts.relaxReleaseAge → 跳过 24h 新版本冷却期。 */
66
+ function runPnpmAdd(pnpm, spec, opts = {}) {
67
+ return new Promise((resolve) => {
68
+ const started = Date.now();
69
+ const stdout = [];
70
+ const stderr = [];
71
+ let child;
72
+ const relax = opts.relaxReleaseAge ? ['--config.minimum-release-age=0'] : [];
73
+ const args = pnpm.kind === 'script'
74
+ ? ['--expose-internals', pnpm.script, 'add', spec, '--save-exact', ...relax]
75
+ : ['add', spec, '--save-exact', ...relax];
76
+ try {
77
+ child = pnpm.kind === 'script'
78
+ ? spawn(pnpm.node, args, { cwd: PROFILE_DIR, windowsHide: true, env: { ...process.env, ELECTRON_RUN_AS_NODE: '1' } })
79
+ : spawn(pnpm.command, args, { cwd: PROFILE_DIR, windowsHide: true, shell: process.platform === 'win32' });
80
+ } catch (e) {
81
+ resolve({ ok: false, stdout: '', stderr: String(e?.message ?? e) });
82
+ return;
83
+ }
84
+ const timer = setTimeout(() => { try { child.kill(); } catch { /* 忽略 */ } }, 240000);
85
+ child.stdout?.setEncoding('utf8');
86
+ child.stderr?.setEncoding('utf8');
87
+ child.stdout?.on('data', (d) => stdout.push(String(d)));
88
+ child.stderr?.on('data', (d) => stderr.push(String(d)));
89
+ child.on('error', (e) => { clearTimeout(timer); resolve({ ok: false, stdout: stdout.join(''), stderr: String(e?.message ?? e) }); });
90
+ child.on('close', (code) => {
91
+ clearTimeout(timer);
92
+ resolve({ ok: code === 0, code, stdout: stdout.join(''), stderr: stderr.join(''), ms: Date.now() - started });
93
+ });
94
+ });
95
+ }
96
+
97
+ /** pnpm add 成功后同步 bundles 列表(等价 DSH reconcile 的 add 分支:新依赖必须成为 bundle 层)。 */
98
+ async function ensureBundleEntry(profileDir, pkg) {
99
+ const manifestPath = path.join(profileDir, 'package.json');
100
+ const m = JSON.parse(await fs.readFile(manifestPath, 'utf8'));
101
+ const bundles = (m.dsh && m.dsh.profile && m.dsh.profile.bundles) || [];
102
+ if (bundles.includes(pkg)) return false;
103
+ bundles.push(pkg);
104
+ m.dsh = { ...(m.dsh || {}), profile: { ...(m.dsh.profile || {}), bundles } };
105
+ await fs.writeFile(manifestPath, JSON.stringify(m, null, 2) + '\n', 'utf8');
106
+ return true;
107
+ }
108
+
109
+ /** 读取 npm registry 上 latest dist-tag 的版本号。 */
110
+ async function npmLatestVersion() {
111
+ const res = await fetch(NPM_LATEST_URL, {
112
+ headers: { accept: 'application/json' },
113
+ signal: AbortSignal.timeout(15000),
114
+ });
115
+ if (!res.ok) throw new Error('npm registry 返回 HTTP ' + res.status);
116
+ const meta = await res.json();
117
+ if (!meta || typeof meta.version !== 'string') throw new Error('npm registry 响应缺少 version 字段');
118
+ return meta.version;
119
+ }
120
+
121
+ /** 语义化版本比较:a<b → -1,a>b → 1,相等或无法解析 → 0;prerelease 低于同号正式版。 */
122
+ function cmpVer(a, b) {
123
+ const parse = (v) => {
124
+ const m = /^v?(\d+)\.(\d+)\.(\d+)(?:-([0-9A-Za-z.-]+))?(?:\+[0-9A-Za-z.-]+)?$/.exec(String(v ?? '').trim());
125
+ return m ? { core: [+m[1], +m[2], +m[3]], pre: m[4] ?? null } : null;
126
+ };
127
+ const A = parse(a);
128
+ const B = parse(b);
129
+ if (!A || !B) return 0;
130
+ for (let i = 0; i < 3; i++) {
131
+ if (A.core[i] !== B.core[i]) return A.core[i] < B.core[i] ? -1 : 1;
132
+ }
133
+ if (A.pre === B.pre) return 0;
134
+ if (A.pre === null) return 1;
135
+ if (B.pre === null) return -1;
136
+ return A.pre < B.pre ? -1 : A.pre > B.pre ? 1 : 0;
137
+ }
138
+
139
+ function readLocalVersion() {
140
+ try {
141
+ return JSON.parse(readFileSync(path.join(HERE, 'package.json'), 'utf8')).version ?? VERSION;
142
+ } catch { return VERSION; }
143
+ }
144
+
145
+ /**
146
+ * 更新检查(与安装方式无关):当前副本版本 vs npm registry latest。
147
+ * 本地版本 ≥ 远端 → 最新;仅真正落后才提示。
148
+ */
149
+ async function updateCheck() {
150
+ const runningVersion = readLocalVersion();
151
+ const remoteVersion = await npmLatestVersion();
152
+ const behind = cmpVer(runningVersion, remoteVersion) < 0 ? 1 : 0;
153
+ return { source: 'npm', runningVersion, remoteVersion, behind, upToDate: behind === 0 };
154
+ }
155
+
156
+ /**
157
+ * 点击即更新(仅在用户显式点击时由客户端调用,无 RPC 参数):
158
+ * 安装 registry latest 的精确版本到所属 profile,绝不自动重启/刷新。
159
+ * - 已是最新 → { ok:true, changed:false }
160
+ * - 受管安装 + pnpm 可用 → pnpm add <pkg>@<exact> --save-exact + 同步 bundles 列表
161
+ * - 开发态 / pnpm 不可用 / 安装失败 → { ok:false, guide:true }(客户端回退为纯引导文案)
162
+ */
163
+ async function updateInstall() {
164
+ const runningVersion = readLocalVersion();
165
+ const latest = await npmLatestVersion();
166
+ if (cmpVer(runningVersion, latest) >= 0) {
167
+ return { ok: true, changed: false, version: runningVersion };
168
+ }
169
+ if (!PROFILE_DIR) {
170
+ return { ok: false, guide: true, version: latest, output: '开发态安装,请自行更新工作区' };
171
+ }
172
+ const pnpm = findPnpm();
173
+ const spec = PKG_NAME + '@' + latest;
174
+ let res = await runPnpmAdd(pnpm, spec);
175
+ let policyRelaxed = false;
176
+ if (!res.ok && /MINIMUM_RELEASE_AGE_VIOLATION/.test((res.stdout || '') + (res.stderr || ''))) {
177
+ // pnpm 11 供应链保护对 24h 内新发布间歇性拦截;用户点击已是明确同意 → 放宽重试一次
178
+ policyRelaxed = true;
179
+ res = await runPnpmAdd(pnpm, spec, { relaxReleaseAge: true });
180
+ }
181
+ const output = ((res.stdout || '') + '\n' + (res.stderr || '')).trim().slice(-1500);
182
+ if (!res.ok) {
183
+ return { ok: false, guide: true, version: latest, output };
184
+ }
185
+ try {
186
+ await ensureBundleEntry(PROFILE_DIR, PKG_NAME);
187
+ } catch { /* bundles 同步失败不阻塞安装结果 */ }
188
+ let installed = latest;
189
+ try {
190
+ installed = JSON.parse(readFileSync(path.join(PROFILE_DIR, 'node_modules', ...PKG_NAME.split('/'), 'package.json'), 'utf8')).version || latest;
191
+ } catch { /* 保留 latest */ }
192
+ return { ok: true, changed: true, version: installed, output, policyRelaxed };
193
+ }
194
+
195
+ export const inject = ['webServer'];
196
+
197
+ // 仅供诊断 / 自动化测试(不属于 RPC 面,运行时行为不受影响)
198
+ export const _internals = { updateCheck, updateInstall, npmLatestVersion, cmpVer, findPnpm, getProfileDir: () => PROFILE_DIR, getVersion: () => VERSION };
199
+
200
+ function resolveModel(providers, want) {
201
+ const byProvider = want && want.provider
202
+ ? providers.find((p) => p.id === want.provider)
203
+ : null;
204
+ const provider = byProvider || providers.find((p) => (p.models ?? []).length) || null;
205
+ if (!provider) return null;
206
+ const byModel = want && want.model
207
+ ? provider.models.find((m) => m.id === want.model)
208
+ : null;
209
+ const model = byModel || provider.models[0] || null;
210
+ if (!model) return null;
211
+ return { provider: provider.id, model: model.id };
212
+ }
213
+
214
+ export function apply(ctx) {
215
+ ctx.effect(() => {
216
+ const llm = () => ctx.get('llm');
217
+
218
+ async function listLlmOptions() {
219
+ const service = llm();
220
+ if (!service) return { providers: [], resolved: null, error: 'llm 服务不可用' };
221
+ const out = [];
222
+ const seen = new Set();
223
+ const pushProvider = async (id, name) => {
224
+ if (!id || seen.has(id)) return;
225
+ seen.add(id);
226
+ try {
227
+ const models = await service.listModels(id);
228
+ out.push({
229
+ id,
230
+ name: name ?? id,
231
+ models: (models ?? []).map((m) => ({ id: m.id, name: m.name ?? m.id })),
232
+ });
233
+ } catch (e) {
234
+ out.push({ id, name: name ?? id, models: [], error: String((e && e.message) || e) });
235
+ }
236
+ };
237
+ try {
238
+ for (const p of (service.listProviders() ?? [])) await pushProvider(p.id, p.name ?? p.id);
239
+ } catch { /* 继续尝试 configurable providers */ }
240
+ try {
241
+ if (typeof service.listConfigurableProviders === 'function') {
242
+ for (const c of (service.listConfigurableProviders() ?? [])) {
243
+ const id = c && (c.id ?? c.key ?? c.settingsNs ?? c.name);
244
+ if (id) await pushProvider(String(id), (c.name ?? c.title) ?? String(id));
245
+ }
246
+ }
247
+ } catch { /* ignore */ }
248
+ return { providers: out, resolved: resolveModel(out) };
249
+ }
250
+
251
+ async function complete(args) {
252
+ const service = llm();
253
+ if (!service) throw new Error('llm 服务不可用');
254
+ const want = args ?? {};
255
+ const { providers } = await listLlmOptions();
256
+ const resolved = resolveModel(providers, want);
257
+ if (!resolved) throw new Error('没有可用模型,请先在 Harness 设置中配置模型提供方');
258
+ let text = '';
259
+ const stream = service.stream({
260
+ provider: resolved.provider,
261
+ model: resolved.model,
262
+ messages: [
263
+ { role: 'system', content: [{ type: 'text', text: String(want.system ?? '') }] },
264
+ { role: 'user', content: [{ type: 'text', text: String(want.user ?? '') }] },
265
+ ],
266
+ temperature: typeof want.temperature === 'number' ? want.temperature : 0.7,
267
+ maxTokens: typeof want.maxTokens === 'number' ? want.maxTokens : 4000,
268
+ });
269
+ for await (const chunk of stream) {
270
+ if (chunk.type === 'text-delta') text += chunk.text;
271
+ }
272
+ return { text, provider: resolved.provider, model: resolved.model };
273
+ }
274
+
275
+ const methods = {
276
+ async ping() { return { ok: true, version: VERSION }; },
277
+ async llmOptions() { return listLlmOptions(); },
278
+ async llmComplete(args) { return complete(args); },
279
+ async updateCheck() { return updateCheck(); },
280
+ async updateInstall() { return updateInstall(); },
281
+ async listSkills() {
282
+ const skills = ctx.get('skills');
283
+ if (!skills) return { skills: [], error: 'skills 服务不可用' };
284
+ try {
285
+ const list = await skills.list({ limit: 200 });
286
+ return {
287
+ skills: (list ?? []).map((s) => ({ name: s.name, description: s.description ?? '' })),
288
+ };
289
+ } catch (e) {
290
+ return { skills: [], error: String((e && e.message) || e) };
291
+ }
292
+ },
293
+ async readSkill(args) {
294
+ const skills = ctx.get('skills');
295
+ if (!skills) throw new Error('skills 服务不可用');
296
+ const name = String((args && args.name) ?? '');
297
+ if (!name) throw new Error('缺少技能名');
298
+ const def = await skills.get(name);
299
+ if (!def) throw new Error('技能不存在: ' + name);
300
+ const text = typeof def === 'string'
301
+ ? def
302
+ : (def.content ?? def.body ?? def.instructions ?? def.markdown ?? null);
303
+ return {
304
+ name,
305
+ text: String(text ?? ((def && typeof def === 'object') ? JSON.stringify(def) : '')).slice(0, 80000),
306
+ };
307
+ },
308
+ };
309
+
310
+ return ctx.webServer.register({
311
+ kind: 'exact',
312
+ path: `/${NAME}/api/rpc`,
313
+ async handler(req, res) {
314
+ res.setHeader('content-type', 'application/json; charset=utf-8');
315
+ try {
316
+ if (req.method !== 'POST') {
317
+ res.statusCode = 405;
318
+ res.end(JSON.stringify({ error: '仅接受 POST' }));
319
+ return;
320
+ }
321
+ let raw = '';
322
+ for await (const chunk of req) {
323
+ raw += chunk;
324
+ if (raw.length > 2e6) throw new Error('请求过大');
325
+ }
326
+ const { method, args } = JSON.parse(raw);
327
+ const fn = methods[method];
328
+ if (!fn) throw new Error('未知操作: ' + method);
329
+ res.end(JSON.stringify(await fn(args ?? {})));
330
+ } catch (e) {
331
+ res.statusCode = 400;
332
+ res.end(JSON.stringify({ error: String((e && e.message) || e) }));
333
+ }
334
+ },
335
+ });
336
+ }, `${NAME}: api route`);
337
+ }
package/locale/en.json CHANGED
@@ -1,6 +1,6 @@
1
- {
2
- "meta": {
3
- "title": "Infinite Canvas",
4
- "description": "Node-based image & video generation workflow on an infinite canvas, with project and asset management in a right sidebar."
5
- }
6
- }
1
+ {
2
+ "meta": {
3
+ "title": "Infinite Canvas",
4
+ "description": "Node-based image & video generation workflow on an infinite canvas, with project and asset management in a right sidebar."
5
+ }
6
+ }
package/locale/zh.json CHANGED
@@ -1,6 +1,6 @@
1
- {
2
- "meta": {
3
- "title": "无限画布",
4
- "description": "无限画布上的节点式图片/视频生成工作流:文本、图片生成、视频生成、图片资产、视频资产节点自由连线,右侧边栏管理项目与资产。"
5
- }
6
- }
1
+ {
2
+ "meta": {
3
+ "title": "无限画布",
4
+ "description": "无限画布上的节点式图片/视频生成工作流:文本、图片生成、视频生成、图片资产、视频资产节点自由连线,右侧边栏管理项目与资产。"
5
+ }
6
+ }