@ninkch/dsh-infinite-canvas 0.17.0 → 0.17.2

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,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 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 { readFileSync } from 'node:fs';
10
+ import path from 'node:path';
11
+ import { fileURLToPath } from 'node:url';
12
+
13
+ const NAME = 'infinite-canvas';
14
+ const HERE = path.dirname(fileURLToPath(import.meta.url));
15
+
16
+ // 版本单一来源 = 当前副本的 package.json(硬编码常量曾与包版本漂移)
17
+ let VERSION = '0.0.0';
18
+ try {
19
+ VERSION = JSON.parse(readFileSync(path.join(HERE, 'package.json'), 'utf8')).version || VERSION;
20
+ } catch { /* 保留默认值 */ }
21
+
22
+ // 更新检查的唯一信息源 = npm registry latest(与安装方式无关:
23
+ // npm 在线安装 / 本地 link 开发态 / 其它方式,一律走同一条逻辑)。
24
+ const NPM_LATEST_URL = 'https://registry.npmjs.org/' + encodeURIComponent('@ninkch/dsh-infinite-canvas') + '/latest';
25
+
26
+ /** 读取 npm registry 上 latest dist-tag 的版本号。 */
27
+ async function npmLatestVersion() {
28
+ const res = await fetch(NPM_LATEST_URL, {
29
+ headers: { accept: 'application/json' },
30
+ signal: AbortSignal.timeout(15000),
31
+ });
32
+ if (!res.ok) throw new Error('npm registry 返回 HTTP ' + res.status);
33
+ const meta = await res.json();
34
+ if (!meta || typeof meta.version !== 'string') throw new Error('npm registry 响应缺少 version 字段');
35
+ return meta.version;
36
+ }
37
+
38
+ /** 语义化版本比较:a<b → -1,a>b → 1,相等或无法解析 → 0;prerelease 低于同号正式版。 */
39
+ function cmpVer(a, b) {
40
+ const parse = (v) => {
41
+ const m = /^v?(\d+)\.(\d+)\.(\d+)(?:-([0-9A-Za-z.-]+))?(?:\+[0-9A-Za-z.-]+)?$/.exec(String(v ?? '').trim());
42
+ return m ? { core: [+m[1], +m[2], +m[3]], pre: m[4] ?? null } : null;
43
+ };
44
+ const A = parse(a);
45
+ const B = parse(b);
46
+ if (!A || !B) return 0;
47
+ for (let i = 0; i < 3; i++) {
48
+ if (A.core[i] !== B.core[i]) return A.core[i] < B.core[i] ? -1 : 1;
49
+ }
50
+ if (A.pre === B.pre) return 0;
51
+ if (A.pre === null) return 1;
52
+ if (B.pre === null) return -1;
53
+ return A.pre < B.pre ? -1 : A.pre > B.pre ? 1 : 0;
54
+ }
55
+
56
+ function readLocalVersion() {
57
+ try {
58
+ return JSON.parse(readFileSync(path.join(HERE, 'package.json'), 'utf8')).version ?? VERSION;
59
+ } catch { return VERSION; }
60
+ }
61
+
62
+ /**
63
+ * 更新检查(与安装方式无关):当前副本版本 vs npm registry latest。
64
+ * 本地版本 ≥ 远端 → 最新;仅真正落后才提示。
65
+ * 插件绝不自动安装、绝不自动重启/刷新 —— 安装一律引导到 DSH 插件管理。
66
+ */
67
+ async function updateCheck() {
68
+ const runningVersion = readLocalVersion();
69
+ const remoteVersion = await npmLatestVersion();
70
+ const behind = cmpVer(runningVersion, remoteVersion) < 0 ? 1 : 0;
71
+ return { source: 'npm', runningVersion, remoteVersion, behind, upToDate: behind === 0 };
72
+ }
73
+
74
+ export const inject = ['webServer'];
75
+
76
+ // 仅供诊断 / 自动化测试(不属于 RPC 面,运行时行为不受影响)
77
+ export const _internals = { updateCheck, npmLatestVersion, cmpVer, getVersion: () => VERSION };
78
+
79
+ function resolveModel(providers, want) {
80
+ const byProvider = want && want.provider
81
+ ? providers.find((p) => p.id === want.provider)
82
+ : null;
83
+ const provider = byProvider || providers.find((p) => (p.models ?? []).length) || null;
84
+ if (!provider) return null;
85
+ const byModel = want && want.model
86
+ ? provider.models.find((m) => m.id === want.model)
87
+ : null;
88
+ const model = byModel || provider.models[0] || null;
89
+ if (!model) return null;
90
+ return { provider: provider.id, model: model.id };
91
+ }
92
+
93
+ export function apply(ctx) {
94
+ ctx.effect(() => {
95
+ const llm = () => ctx.get('llm');
96
+
97
+ async function listLlmOptions() {
98
+ const service = llm();
99
+ if (!service) return { providers: [], resolved: null, error: 'llm 服务不可用' };
100
+ const out = [];
101
+ const seen = new Set();
102
+ const pushProvider = async (id, name) => {
103
+ if (!id || seen.has(id)) return;
104
+ seen.add(id);
105
+ try {
106
+ const models = await service.listModels(id);
107
+ out.push({
108
+ id,
109
+ name: name ?? id,
110
+ models: (models ?? []).map((m) => ({ id: m.id, name: m.name ?? m.id })),
111
+ });
112
+ } catch (e) {
113
+ out.push({ id, name: name ?? id, models: [], error: String((e && e.message) || e) });
114
+ }
115
+ };
116
+ try {
117
+ for (const p of (service.listProviders() ?? [])) await pushProvider(p.id, p.name ?? p.id);
118
+ } catch { /* 继续尝试 configurable providers */ }
119
+ try {
120
+ if (typeof service.listConfigurableProviders === 'function') {
121
+ for (const c of (service.listConfigurableProviders() ?? [])) {
122
+ const id = c && (c.id ?? c.key ?? c.settingsNs ?? c.name);
123
+ if (id) await pushProvider(String(id), (c.name ?? c.title) ?? String(id));
124
+ }
125
+ }
126
+ } catch { /* ignore */ }
127
+ return { providers: out, resolved: resolveModel(out) };
128
+ }
129
+
130
+ async function complete(args) {
131
+ const service = llm();
132
+ if (!service) throw new Error('llm 服务不可用');
133
+ const want = args ?? {};
134
+ const { providers } = await listLlmOptions();
135
+ const resolved = resolveModel(providers, want);
136
+ if (!resolved) throw new Error('没有可用模型,请先在 Harness 设置中配置模型提供方');
137
+ let text = '';
138
+ const stream = service.stream({
139
+ provider: resolved.provider,
140
+ model: resolved.model,
141
+ messages: [
142
+ { role: 'system', content: [{ type: 'text', text: String(want.system ?? '') }] },
143
+ { role: 'user', content: [{ type: 'text', text: String(want.user ?? '') }] },
144
+ ],
145
+ temperature: typeof want.temperature === 'number' ? want.temperature : 0.7,
146
+ maxTokens: typeof want.maxTokens === 'number' ? want.maxTokens : 4000,
147
+ });
148
+ for await (const chunk of stream) {
149
+ if (chunk.type === 'text-delta') text += chunk.text;
150
+ }
151
+ return { text, provider: resolved.provider, model: resolved.model };
152
+ }
153
+
154
+ const methods = {
155
+ async ping() { return { ok: true, version: VERSION }; },
156
+ async llmOptions() { return listLlmOptions(); },
157
+ async llmComplete(args) { return complete(args); },
158
+ async updateCheck() { return updateCheck(); },
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
+ }
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
+ }
package/package.json CHANGED
@@ -1,55 +1,55 @@
1
- {
2
- "name": "@ninkch/dsh-infinite-canvas",
3
- "version": "0.17.0",
4
- "description": "Node-based short-drama studio for DeepSeek Harness: script → storyboard → assets → shots → voiced, subtitled final cut on an infinite canvas.",
5
- "keywords": [
6
- "dsh",
7
- "deepseek-harness",
8
- "plugin",
9
- "infinite-canvas",
10
- "storyboard",
11
- "short-drama",
12
- "ai-video",
13
- "image-generation",
14
- "video-generation",
15
- "seedance",
16
- "photocraft",
17
- "filmcraft"
18
- ],
19
- "license": "Apache-2.0",
20
- "author": "NINKCH",
21
- "publishConfig": {
22
- "registry": "https://registry.npmjs.org/"
23
- },
24
- "repository": {
25
- "type": "git",
26
- "url": "git+https://github.com/NINKCH/dsh-infinite_canvas.git"
27
- },
28
- "type": "module",
29
- "icon": "./icon.svg",
30
- "exports": {
31
- ".": "./index.js",
32
- "./client": "./client.js",
33
- "./package.json": "./package.json",
34
- "./locale/*.json": "./locale/*.json"
35
- },
36
- "files": [
37
- "index.js",
38
- "client.js",
39
- "icon.svg",
40
- "cordis.patch.yml",
41
- "locale/*.json",
42
- "README.md",
43
- "README.zh-CN.md",
44
- "docs/*.md"
45
- ],
46
- "dsh": {
47
- "bundle": {
48
- "patch": "./cordis.patch.yml"
49
- },
50
- "client": {
51
- "platform": "web",
52
- "immediately": true
53
- }
54
- }
55
- }
1
+ {
2
+ "name": "@ninkch/dsh-infinite-canvas",
3
+ "version": "0.17.2",
4
+ "description": "Node-based short-drama studio for DeepSeek Harness: script → storyboard → assets → shots → voiced, subtitled final cut on an infinite canvas.",
5
+ "keywords": [
6
+ "dsh",
7
+ "deepseek-harness",
8
+ "plugin",
9
+ "infinite-canvas",
10
+ "storyboard",
11
+ "short-drama",
12
+ "ai-video",
13
+ "image-generation",
14
+ "video-generation",
15
+ "seedance",
16
+ "photocraft",
17
+ "filmcraft"
18
+ ],
19
+ "license": "Apache-2.0",
20
+ "author": "NINKCH",
21
+ "publishConfig": {
22
+ "registry": "https://registry.npmjs.org/"
23
+ },
24
+ "repository": {
25
+ "type": "git",
26
+ "url": "git+https://github.com/NINKCH/dsh-infinite_canvas.git"
27
+ },
28
+ "type": "module",
29
+ "icon": "./icon.svg",
30
+ "exports": {
31
+ ".": "./index.js",
32
+ "./client": "./client.js",
33
+ "./package.json": "./package.json",
34
+ "./locale/*.json": "./locale/*.json"
35
+ },
36
+ "files": [
37
+ "index.js",
38
+ "client.js",
39
+ "icon.svg",
40
+ "cordis.patch.yml",
41
+ "locale/*.json",
42
+ "README.md",
43
+ "README.zh-CN.md",
44
+ "docs/*.md"
45
+ ],
46
+ "dsh": {
47
+ "bundle": {
48
+ "patch": "./cordis.patch.yml"
49
+ },
50
+ "client": {
51
+ "platform": "web",
52
+ "immediately": true
53
+ }
54
+ }
55
+ }