@mokuyoaxis/dsh-iris 0.1.0 → 0.1.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.
- package/CHANGELOG.md +31 -1
- package/README.md +28 -9
- package/docs/ROADMAP.md +31 -20
- package/docs/releases/0.1.1.md +51 -0
- package/lib/actions.js +33 -27
- package/lib/adapters.js +42 -26
- package/lib/api.js +31 -9
- package/lib/capability.js +1 -1
- package/lib/client.js +26 -17
- package/lib/config.js +56 -22
- package/lib/guard.js +37 -24
- package/lib/index.js +10 -8
- package/lib/locate.js +1 -1
- package/lib/media-probe.js +4 -4
- package/lib/media.js +1 -1
- package/lib/private-storage.js +77 -0
- package/lib/provider-protocol.js +25 -0
- package/lib/render.js +3 -2
- package/lib/tasks.js +21 -13
- package/lib/vision.js +2 -2
- package/package.json +19 -15
- package/user_guide.md +34 -9
package/lib/index.js
CHANGED
|
@@ -7,7 +7,6 @@
|
|
|
7
7
|
* 架构纪律:无独立后台,与 DSH 俱荣俱损——定时器归 Fiber,产物落 $DSH_HOME。
|
|
8
8
|
*/
|
|
9
9
|
import fs from 'node:fs';
|
|
10
|
-
import os from 'node:os';
|
|
11
10
|
import path from 'node:path';
|
|
12
11
|
import * as adapters from './adapters.js';
|
|
13
12
|
import * as store from './config.js';
|
|
@@ -19,6 +18,7 @@ import { buildVisionBackends, askWithBackends } from './vision.js';
|
|
|
19
18
|
import { cropImage, pixelDiff, imageDimensions } from './pixels.js';
|
|
20
19
|
import { locateObject } from './locate.js';
|
|
21
20
|
import { serveRender, BrowserHtmlRenderer } from './render.js';
|
|
21
|
+
import { ensurePrivateDir, hardenPrivateTree } from './private-storage.js';
|
|
22
22
|
import { guarded } from './guard.js';
|
|
23
23
|
import { longOcr } from './ocr.js';
|
|
24
24
|
import { runAction, listActions } from './actions.js';
|
|
@@ -38,8 +38,7 @@ const AWAIT_MS = { image: 180000, video: 480000 };
|
|
|
38
38
|
|
|
39
39
|
function outputDir() {
|
|
40
40
|
const dir = tasks.outputsDir();
|
|
41
|
-
|
|
42
|
-
return dir;
|
|
41
|
+
return ensurePrivateDir(dir);
|
|
43
42
|
}
|
|
44
43
|
|
|
45
44
|
/* ---------------- 媒体路由:/iris/media/:taskId/:token/:name ---------------- */
|
|
@@ -90,7 +89,7 @@ export function pollDeps(provider, cap) {
|
|
|
90
89
|
return {
|
|
91
90
|
key: () => provider.apiKey,
|
|
92
91
|
poll: async ({ key, remoteTaskId }) => {
|
|
93
|
-
const r = await adapters.pollTranscriptionTask({ key, remoteTaskId });
|
|
92
|
+
const r = await adapters.pollTranscriptionTask({ key, baseUrl: provider.baseUrl, remoteTaskId });
|
|
94
93
|
return { done: r.done, ok: r.ok, urls: r.text ? [r.text] : [], message: r.message };
|
|
95
94
|
},
|
|
96
95
|
intervalMs: 2500,
|
|
@@ -102,7 +101,7 @@ export function pollDeps(provider, cap) {
|
|
|
102
101
|
}
|
|
103
102
|
return {
|
|
104
103
|
key: () => provider.apiKey,
|
|
105
|
-
poll: ({ key, remoteTaskId }) => adapters.pollTask({ key, remoteTaskId }),
|
|
104
|
+
poll: ({ key, remoteTaskId }) => adapters.pollTask({ key, baseUrl: provider.baseUrl, remoteTaskId }),
|
|
106
105
|
intervalMs: cap === 'video' ? 6000 : 2500,
|
|
107
106
|
onSuccess: async (t, r) => {
|
|
108
107
|
fs.mkdirSync(outputDir(), { recursive: true });
|
|
@@ -181,14 +180,18 @@ function safeRegister(ctx, definition) {
|
|
|
181
180
|
}
|
|
182
181
|
|
|
183
182
|
export async function apply(ctx) {
|
|
183
|
+
guard('Iris 私有存储权限收紧', () => hardenPrivateTree(store.irisHome()));
|
|
184
184
|
|
|
185
185
|
guard('上传临时文件清理', () => purgeStaleUploads());
|
|
186
186
|
|
|
187
|
-
//
|
|
187
|
+
// 仅按用户显式指定的路径导入旧配置;不扫描其他项目。
|
|
188
188
|
guard('工作台导入', () => {
|
|
189
|
-
const wb =
|
|
189
|
+
const wb = process.env.IRIS_IMPORT_WORKBENCH_CONFIG;
|
|
190
|
+
if (!wb) return;
|
|
191
|
+
if (!path.isAbsolute(wb)) throw new Error('IRIS_IMPORT_WORKBENCH_CONFIG 必须是绝对路径');
|
|
190
192
|
const imported = store.importFromWorkbench(wb);
|
|
191
193
|
if (imported.imported) console.log(`[iris] 已从工作台导入 ${imported.imported} 个服务商`);
|
|
194
|
+
else console.log('[iris] 工作台导入:' + imported.reason);
|
|
192
195
|
});
|
|
193
196
|
|
|
194
197
|
// 接管上次进程未跑完的异步任务(恢复循环本身在 tasks.js 里逐任务防御)
|
|
@@ -1025,4 +1028,3 @@ async function describeFirstImage(ctx, ref, absPath, originalPrompt) {
|
|
|
1025
1028
|
return ''; // 自述是增强项,失败不影响交付
|
|
1026
1029
|
}
|
|
1027
1030
|
}
|
|
1028
|
-
|
package/lib/locate.js
CHANGED
package/lib/media-probe.js
CHANGED
|
@@ -2,11 +2,11 @@
|
|
|
2
2
|
/**
|
|
3
3
|
* Iris 媒体分析探测(阶段 7.1)—— 视频抽帧。
|
|
4
4
|
*
|
|
5
|
-
* ffmpeg
|
|
5
|
+
* ffmpeg 是可选系统可执行文件:启动时直接执行版本探测,缺失则 `ffmpegAvailable()` 返回
|
|
6
6
|
* false,抽帧工具在执行时报清晰的人话错误(其余功能不受影响)。静态版 ffmpeg
|
|
7
7
|
* (johnvansickle)同样可用。本模块为纯后端:零 npm 依赖,可离线单测。
|
|
8
8
|
*
|
|
9
|
-
*
|
|
9
|
+
* 抽帧契约:
|
|
10
10
|
* - 最多帧数上限 20(覆盖 5 分钟短视频,不超 LLM 上下文窗口);
|
|
11
11
|
* - 最大宽度 4096(含),缩放到 targetWidth,高度按比例取偶数;
|
|
12
12
|
* - 帧数 = 请求数(时间均匀采样),短视频也能取到整 N 帧;
|
|
@@ -24,8 +24,8 @@ export const MIN_WIDTH = 16;
|
|
|
24
24
|
/** 探测 ffmpeg / ffprobe 是否可用 */
|
|
25
25
|
export function ffmpegAvailable() {
|
|
26
26
|
try {
|
|
27
|
-
const ff = spawnSync('
|
|
28
|
-
const fp = spawnSync('
|
|
27
|
+
const ff = spawnSync('ffmpeg', ['-version'], { stdio: 'ignore', windowsHide: true });
|
|
28
|
+
const fp = spawnSync('ffprobe', ['-version'], { stdio: 'ignore', windowsHide: true });
|
|
29
29
|
return ff.status === 0 && fp.status === 0;
|
|
30
30
|
} catch (_) {
|
|
31
31
|
return false;
|
package/lib/media.js
CHANGED
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
/**
|
|
3
|
+
* Iris 私有存储原语。
|
|
4
|
+
*
|
|
5
|
+
* POSIX 上目录统一 0700、文件统一 0600;Windows 的 mode 不能替代 ACL,
|
|
6
|
+
* 仍在创建时传入最小 mode,但公开文档不会把它描述成 Windows 访问控制。
|
|
7
|
+
*/
|
|
8
|
+
import crypto from 'node:crypto';
|
|
9
|
+
import fs from 'node:fs';
|
|
10
|
+
import path from 'node:path';
|
|
11
|
+
|
|
12
|
+
const HAS_POSIX_MODE = process.platform !== 'win32';
|
|
13
|
+
|
|
14
|
+
function tryChmod(target, mode) {
|
|
15
|
+
if (!HAS_POSIX_MODE) return;
|
|
16
|
+
try { fs.chmodSync(target, mode); } catch (_) { /* 调用方仍可继续使用宿主文件系统 */ }
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export function ensurePrivateDir(dir) {
|
|
20
|
+
const existed = fs.existsSync(dir);
|
|
21
|
+
fs.mkdirSync(dir, { recursive: true, mode: 0o700 });
|
|
22
|
+
// 不擅自 chmod 调用方传入的既有父目录(例如适配器单测使用 os.tmpdir)。
|
|
23
|
+
// Iris 自有既有树由启动时 hardenPrivateTree() 明确收紧。
|
|
24
|
+
if (!existed) tryChmod(dir, 0o700);
|
|
25
|
+
return dir;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export function chmodPrivateFile(file) {
|
|
29
|
+
tryChmod(file, 0o600);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export function writePrivateFile(file, data, options = {}) {
|
|
33
|
+
ensurePrivateDir(path.dirname(file));
|
|
34
|
+
fs.writeFileSync(file, data, { ...options, mode: 0o600 });
|
|
35
|
+
chmodPrivateFile(file);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export function privateSibling(file, label = 'tmp') {
|
|
39
|
+
return `${file}.${label}-${process.pid}-${crypto.randomBytes(6).toString('hex')}`;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export function atomicWritePrivate(file, data) {
|
|
43
|
+
ensurePrivateDir(path.dirname(file));
|
|
44
|
+
const tmp = privateSibling(file);
|
|
45
|
+
try {
|
|
46
|
+
fs.writeFileSync(tmp, data, { mode: 0o600, flag: 'wx' });
|
|
47
|
+
fs.renameSync(tmp, file);
|
|
48
|
+
chmodPrivateFile(file);
|
|
49
|
+
} finally {
|
|
50
|
+
try { fs.rmSync(tmp, { force: true }); } catch (_) { /* rename 成功后临时文件已不存在 */ }
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/** 收紧既有 Iris 树;不跟随符号链接,也不修改文件内容。 */
|
|
55
|
+
export function hardenPrivateTree(root) {
|
|
56
|
+
ensurePrivateDir(root);
|
|
57
|
+
if (!HAS_POSIX_MODE) return { directories: 0, files: 0, skipped: true };
|
|
58
|
+
let directories = 0;
|
|
59
|
+
let files = 0;
|
|
60
|
+
const walk = (target) => {
|
|
61
|
+
let st;
|
|
62
|
+
try { st = fs.lstatSync(target); } catch (_) { return; }
|
|
63
|
+
if (st.isSymbolicLink()) return;
|
|
64
|
+
if (st.isDirectory()) {
|
|
65
|
+
tryChmod(target, 0o700);
|
|
66
|
+
directories++;
|
|
67
|
+
for (const name of fs.readdirSync(target)) walk(path.join(target, name));
|
|
68
|
+
return;
|
|
69
|
+
}
|
|
70
|
+
if (st.isFile()) {
|
|
71
|
+
tryChmod(target, 0o600);
|
|
72
|
+
files++;
|
|
73
|
+
}
|
|
74
|
+
};
|
|
75
|
+
walk(root);
|
|
76
|
+
return { directories, files, skipped: false };
|
|
77
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
/** 供应商媒体协议判断与 DashScope 凭据边界。 */
|
|
3
|
+
|
|
4
|
+
const DASH_SCOPE_HOST = /^dashscope(?:-[a-z0-9-]+)?\.aliyuncs\.com$/i;
|
|
5
|
+
|
|
6
|
+
export function isDashScopeBaseUrl(value) {
|
|
7
|
+
try {
|
|
8
|
+
const url = new URL(String(value || '').trim());
|
|
9
|
+
return url.protocol === 'https:' && !url.username && !url.password && DASH_SCOPE_HOST.test(url.hostname);
|
|
10
|
+
} catch (_) {
|
|
11
|
+
return false;
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export function inferMediaProtocol(baseUrl) {
|
|
16
|
+
return isDashScopeBaseUrl(baseUrl) ? 'dashscope' : 'openai-images';
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/** 返回官方 DashScope API 根;不满足边界时在 fetch 前拒绝。 */
|
|
20
|
+
export function dashscopeApiBase(baseUrl) {
|
|
21
|
+
if (!isDashScopeBaseUrl(baseUrl)) {
|
|
22
|
+
throw new Error('iris: DashScope 协议只允许使用阿里云官方 HTTPS Base URL;已阻止发送 API Key');
|
|
23
|
+
}
|
|
24
|
+
return new URL('/api/v1', String(baseUrl).trim()).toString().replace(/\/$/, '');
|
|
25
|
+
}
|
package/lib/render.js
CHANGED
|
@@ -16,6 +16,7 @@ import fs from 'node:fs';
|
|
|
16
16
|
import path from 'node:path';
|
|
17
17
|
import { irisHome } from './config.js';
|
|
18
18
|
import { webBase, mimeOf } from './media.js';
|
|
19
|
+
import { ensurePrivateDir, writePrivateFile } from './private-storage.js';
|
|
19
20
|
|
|
20
21
|
export class RenderError extends Error {
|
|
21
22
|
constructor(message) {
|
|
@@ -49,7 +50,7 @@ export class BrowserHtmlRenderer {
|
|
|
49
50
|
|
|
50
51
|
// 每次调用独立子目录(随机名),写 index.html
|
|
51
52
|
const dir = path.join(renderRoot(), crypto.randomBytes(6).toString('hex'));
|
|
52
|
-
|
|
53
|
+
ensurePrivateDir(dir);
|
|
53
54
|
let content = text;
|
|
54
55
|
if (width || height) {
|
|
55
56
|
const w = width ? Math.floor(width) : 'auto';
|
|
@@ -57,7 +58,7 @@ export class BrowserHtmlRenderer {
|
|
|
57
58
|
content = `<div style="min-width:${w}px;min-height:${h}px">${text}</div>`;
|
|
58
59
|
}
|
|
59
60
|
const htmlPath = path.join(dir, 'index.html');
|
|
60
|
-
|
|
61
|
+
writePrivateFile(htmlPath, content);
|
|
61
62
|
const url = `${webBase()}/iris/render/${path.basename(dir)}/index.html`;
|
|
62
63
|
const pngPath = path.join(dir, 'render.png');
|
|
63
64
|
|
package/lib/tasks.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
/**
|
|
3
|
-
* Iris 任务框架 ——
|
|
3
|
+
* Iris 任务框架 —— 从早期独立工作台演进而来的「宿内版」。
|
|
4
4
|
*
|
|
5
5
|
* 与 DSH 俱荣俱损的三条纪律:
|
|
6
6
|
* - 无独立进程:盯守定时器全部 unref(不拖住 DSH 退出),且随插件 Fiber 清理(stopWatchAll)
|
|
@@ -14,6 +14,7 @@
|
|
|
14
14
|
import fs from 'node:fs';
|
|
15
15
|
import path from 'node:path';
|
|
16
16
|
import { irisHome } from './config.js';
|
|
17
|
+
import { atomicWritePrivate, chmodPrivateFile, privateSibling } from './private-storage.js';
|
|
17
18
|
|
|
18
19
|
const FILE = () => path.join(irisHome(), 'tasks.json');
|
|
19
20
|
/** 产物目录:$DSH_HOME/iris/v1/outputs */
|
|
@@ -46,19 +47,24 @@ function emitChange() {
|
|
|
46
47
|
}
|
|
47
48
|
|
|
48
49
|
function persist() {
|
|
49
|
-
|
|
50
|
-
const tmp = FILE() + '.tmp';
|
|
51
|
-
// 0600 从创建时生效(含临时文件);rename 保留 mode,chmod 兜底不依赖 umask
|
|
52
|
-
fs.writeFileSync(tmp, JSON.stringify({ version: 1, tasks: cache.tasks }, null, 2), { mode: 0o600 });
|
|
53
|
-
fs.renameSync(tmp, FILE());
|
|
54
|
-
try {
|
|
55
|
-
fs.chmodSync(FILE(), 0o600);
|
|
56
|
-
} catch (_) {
|
|
57
|
-
/* ignore */
|
|
58
|
-
}
|
|
50
|
+
atomicWritePrivate(FILE(), JSON.stringify({ version: 1, tasks: cache.tasks }, null, 2));
|
|
59
51
|
emitChange(); // 任何任务落盘 = 状态变了 → SSE 推送
|
|
60
52
|
}
|
|
61
53
|
|
|
54
|
+
function isPlainObject(value) {
|
|
55
|
+
return value !== null && typeof value === 'object' && !Array.isArray(value);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function validateStoredTasks(value) {
|
|
59
|
+
if (!isPlainObject(value)) throw new Error('任务根节点必须是对象');
|
|
60
|
+
if (!Array.isArray(value.tasks)) throw new Error('tasks 必须是数组');
|
|
61
|
+
for (const task of value.tasks) {
|
|
62
|
+
if (!isPlainObject(task) || typeof task.id !== 'string' || !task.id.trim()) {
|
|
63
|
+
throw new Error('task 条目必须是带非空 id 的对象');
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
62
68
|
function load() {
|
|
63
69
|
if (cache) return cache;
|
|
64
70
|
const file = FILE();
|
|
@@ -70,12 +76,14 @@ function load() {
|
|
|
70
76
|
}
|
|
71
77
|
try {
|
|
72
78
|
const raw = JSON.parse(fs.readFileSync(file, 'utf8'));
|
|
73
|
-
|
|
79
|
+
validateStoredTasks(raw);
|
|
80
|
+
cache = { version: 1, tasks: raw.tasks };
|
|
74
81
|
} catch (err) {
|
|
75
82
|
// 文件存在但损坏:隔离,绝不静默覆盖证据
|
|
76
|
-
const backup = file
|
|
83
|
+
const backup = privateSibling(file, 'corrupted');
|
|
77
84
|
try {
|
|
78
85
|
fs.renameSync(file, backup);
|
|
86
|
+
chmodPrivateFile(backup);
|
|
79
87
|
} catch (_) {
|
|
80
88
|
/* 隔离失败也继续 */
|
|
81
89
|
}
|
package/lib/vision.js
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
/**
|
|
3
3
|
* Iris 视觉后端抽象(阶段 1)。
|
|
4
4
|
*
|
|
5
|
-
*
|
|
5
|
+
* 把“识图”拆成「VisionBackend 接口 +
|
|
6
6
|
* 两个实现」,让自持栈与 DSH 全局模型可替换、按序降级。
|
|
7
7
|
* - SelfStackVisionBackend:自持栈(OpenAI 兼容 /chat/completions,qwen-vl)
|
|
8
8
|
* - GlobalLlmVisionBackend:DSH 全局视觉模型(ctx.llm.stream())
|
|
@@ -141,7 +141,7 @@ const RED_HINTS = /红|red/i;
|
|
|
141
141
|
|
|
142
142
|
/**
|
|
143
143
|
* 视觉能力测试:向后端发固定红色测试图并提问,验证回答是否识别出红色。
|
|
144
|
-
*
|
|
144
|
+
* 用于用户显式触发的能力实测流程。
|
|
145
145
|
* @param {object} backend VisionBackend 实例(有 analyze 方法)
|
|
146
146
|
* @param {{timeoutMs?:number, signal?:AbortSignal}} opts
|
|
147
147
|
* @returns {Promise<{ok:boolean, answer:string, error?:string, timedOut?:boolean}>}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@mokuyoaxis/dsh-iris",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.1",
|
|
4
4
|
"description": "Give DeepSeek Harness eyes and hands: multi-provider media generation, vision routing, and an integrated Iris workbench.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "lib/index.js",
|
|
@@ -24,16 +24,19 @@
|
|
|
24
24
|
".dsh/skills/iris-compose-media"
|
|
25
25
|
],
|
|
26
26
|
"dsh": {
|
|
27
|
+
"engines": {
|
|
28
|
+
"dsh": ">=0.1.2-rc.1 <0.1.3-0"
|
|
29
|
+
},
|
|
27
30
|
"bundle": {
|
|
28
31
|
"patch": "./cordis.patch.yml"
|
|
29
32
|
},
|
|
30
33
|
"client": {
|
|
31
34
|
"platform": "web",
|
|
32
35
|
"inject": [
|
|
33
|
-
"@deepseek-ai/dsh-client-runtime",
|
|
34
36
|
"@deepseek-ai/dsh-client-locale",
|
|
35
|
-
"@deepseek-ai/dsh-client-ui-slots",
|
|
36
37
|
"@deepseek-ai/dsh-client-ui-conversation",
|
|
38
|
+
"@deepseek-ai/dsh-client-ui-renderer",
|
|
39
|
+
"@deepseek-ai/dsh-client-ui-session",
|
|
37
40
|
"@deepseek-ai/dsh-client-ui-settings"
|
|
38
41
|
]
|
|
39
42
|
}
|
|
@@ -48,30 +51,31 @@
|
|
|
48
51
|
],
|
|
49
52
|
"license": "MIT",
|
|
50
53
|
"engines": {
|
|
51
|
-
"node": ">=20.
|
|
54
|
+
"node": ">=20.10.0"
|
|
52
55
|
},
|
|
53
56
|
"scripts": {
|
|
54
|
-
"test": "
|
|
55
|
-
"lint": "node tests/lint.mjs"
|
|
57
|
+
"test": "node scripts/test.mjs",
|
|
58
|
+
"lint": "node tests/lint.mjs",
|
|
59
|
+
"prepublishOnly": "npm test"
|
|
56
60
|
},
|
|
57
61
|
"peerDependencies": {
|
|
58
|
-
"@deepseek-ai/dsh-client-locale": "
|
|
59
|
-
"@deepseek-ai/dsh-client-
|
|
60
|
-
"@deepseek-ai/dsh-client-ui-
|
|
61
|
-
"@deepseek-ai/dsh-client-ui-
|
|
62
|
-
"@deepseek-ai/dsh-client-ui-
|
|
62
|
+
"@deepseek-ai/dsh-client-locale": "^0.1.2-rc.1",
|
|
63
|
+
"@deepseek-ai/dsh-client-ui-conversation": "^0.1.2-rc.1",
|
|
64
|
+
"@deepseek-ai/dsh-client-ui-renderer": "^0.1.2-rc.1",
|
|
65
|
+
"@deepseek-ai/dsh-client-ui-session": "^0.1.2-rc.1",
|
|
66
|
+
"@deepseek-ai/dsh-client-ui-settings": "^0.1.2-rc.1"
|
|
63
67
|
},
|
|
64
68
|
"peerDependenciesMeta": {
|
|
65
|
-
"@deepseek-ai/dsh-client-
|
|
69
|
+
"@deepseek-ai/dsh-client-locale": {
|
|
66
70
|
"optional": true
|
|
67
71
|
},
|
|
68
|
-
"@deepseek-ai/dsh-client-
|
|
72
|
+
"@deepseek-ai/dsh-client-ui-conversation": {
|
|
69
73
|
"optional": true
|
|
70
74
|
},
|
|
71
|
-
"@deepseek-ai/dsh-client-ui-
|
|
75
|
+
"@deepseek-ai/dsh-client-ui-renderer": {
|
|
72
76
|
"optional": true
|
|
73
77
|
},
|
|
74
|
-
"@deepseek-ai/dsh-client-ui-
|
|
78
|
+
"@deepseek-ai/dsh-client-ui-session": {
|
|
75
79
|
"optional": true
|
|
76
80
|
},
|
|
77
81
|
"@deepseek-ai/dsh-client-ui-settings": {
|
package/user_guide.md
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
|
|
5
5
|
## 安装
|
|
6
6
|
|
|
7
|
-
|
|
7
|
+
你需要 DeepSeek Harness `>=0.1.2-rc.1 <0.1.3-0`、PATH 中的 `pnpm`,以及至少一个媒体或视觉服务供应商。dsh-iris 自身要求 Node.js 20.10 或更高版本;如果所用 DSH 版本要求更高,以 DSH 为准。Iris 0.1.1 不兼容 DSH 0.1.0/0.1.1 的旧客户端 Runtime。
|
|
8
8
|
|
|
9
9
|
从 npm 安装到 Web profile:
|
|
10
10
|
|
|
@@ -20,7 +20,7 @@ dsh plugin --profile web add .
|
|
|
20
20
|
dsh web
|
|
21
21
|
```
|
|
22
22
|
|
|
23
|
-
浏览器默认打开 `http://127.0.0.1:3080`。插件安装后只会在下一次 DSH 启动时装载;如果 DSH
|
|
23
|
+
浏览器默认打开 `http://127.0.0.1:3080`。插件安装后只会在下一次 DSH 启动时装载;如果 DSH 已经运行,请先停止再启动。启动输出包含一次性认证参数时,必须完整打开该 URL,不能只输入裸地址。
|
|
24
24
|
|
|
25
25
|
可以在启动前检查组合配置:
|
|
26
26
|
|
|
@@ -50,10 +50,10 @@ Iris 当前支持两类调用路径:
|
|
|
50
50
|
|
|
51
51
|
| 路径 | 可用能力 | 说明 |
|
|
52
52
|
|---|---|---|
|
|
53
|
-
| DashScope 百炼 | 图片、视频、TTS、转写、视觉 |
|
|
53
|
+
| DashScope 百炼 | 图片、视频、TTS、转写、视觉 | 覆盖最完整;媒体请求只允许阿里云官方 HTTPS 域名 |
|
|
54
54
|
| OpenAI 兼容 | Images 图片生成、Chat Completions 视觉 | 具体模型和能力取决于兼容服务 |
|
|
55
55
|
|
|
56
|
-
|
|
56
|
+
工作台新增供应商时会按 Base URL 安全推断媒体协议:阿里云官方 DashScope HTTPS 域名使用 `dashscope`,其他地址使用 `openai-images`,也可以在供应商卡片中明确选择。即使配置被误标为 `dashscope`,Iris 也会在 fetch 前拒绝向非官方地址发送 DashScope API Key。
|
|
57
57
|
|
|
58
58
|
同一个 OpenAI 兼容 Base URL 只有在实际提供相应接口时才能承担对应能力:图片生成需要 Images 接口,视觉理解需要支持图片输入的 Chat Completions 接口。通用 OpenAI 兼容端点目前不能替代 Iris 的 DashScope 视频、TTS 或转写协议。
|
|
59
59
|
|
|
@@ -73,12 +73,12 @@ Iris 当前支持两类调用路径:
|
|
|
73
73
|
|
|
74
74
|
没有 `/models` 接口时,可以在供应商的模型池中手动添加模型名。Iris 能识别常见的 wan、qwen-vl、qwen-tts、qwen-audio ASR Filetrans、Fun-ASR、paraformer、gpt-image、dall-e 和 Gemini 命名;无法识别的模型需要在高级配置中显式填写 `capabilities`。
|
|
75
75
|
|
|
76
|
-
|
|
76
|
+
点击模型旁单项能力标记会进行验证。视觉、图片和 TTS 会先显示确认框,再发起真实供应商请求,不是本地校验:
|
|
77
77
|
|
|
78
78
|
- 视觉测试会发送一张最小测试图;
|
|
79
79
|
- 图片和 TTS 测试会生成真实产物;
|
|
80
|
-
-
|
|
81
|
-
-
|
|
80
|
+
- 视频不会自动提交探针,因为“只确认受理”也可能创建计费任务;应通过“视频生成”操作验证。
|
|
81
|
+
- 转写不使用空样本探针,应通过“音频转写”操作用真实音频验证。
|
|
82
82
|
|
|
83
83
|
模型不必经过“测”才能使用。这个标记只记录人工触发的实测结果。
|
|
84
84
|
|
|
@@ -93,6 +93,24 @@ Iris 当前支持两类调用路径:
|
|
|
93
93
|
|
|
94
94
|
生成类故障转移只覆盖上传、提交和同步生成阶段。远端一旦受理异步任务,Iris 就会继续跟踪这个任务,不会因轮询或下载失败自动重提,以免重复生成和计费。
|
|
95
95
|
|
|
96
|
+
## 从旧工作台显式导入
|
|
97
|
+
|
|
98
|
+
`ai-paint` 是 Iris 维护者本机未公开的前身项目,不是公开依赖。普通用户不需要、也不应该为了使用 Iris 去下载、安装或自行创建 ai-paint 目录;直接在 Iris 工作台添加供应商即可。
|
|
99
|
+
|
|
100
|
+
旧版读取路径同时承担了本机迁移和密钥保护作用:由运行 DSH 的宿主进程直接读取维护者已有的本地配置,可以避免为了迁移把 API Key 重新粘贴到 Agent 会话、命令行参数或文档中。它不是一个配置服务,也不意味着 Iris 运行时应该长期依赖另一个项目。
|
|
101
|
+
|
|
102
|
+
从 0.1.1 起,Iris 独立保存供应商、密钥、模型池和能力分配,位置为 `$DSH_HOME/iris/v1/providers.json`(未设置 `DSH_HOME` 时使用用户目录下的 `.dsh`)。修改 Iris 配置不会影响 ai-paint,反之亦然。
|
|
103
|
+
|
|
104
|
+
默认启动不会扫描 ai-paint 或其他项目。推荐通过工作台手动添加供应商;需要迁移旧 ai-paint 配置时,先停止目标 DSH 实例并备份其 Iris 配置,再显式指定来源文件的绝对路径。Bash 示例:
|
|
105
|
+
|
|
106
|
+
```bash
|
|
107
|
+
IRIS_IMPORT_WORKBENCH_CONFIG="/absolute/path/ai-paint/data/config.json" dsh web
|
|
108
|
+
```
|
|
109
|
+
|
|
110
|
+
其他 Shell 请使用其环境变量设置方式。`IRIS_IMPORT_WORKBENCH_CONFIG` 的值只是来源配置文件的路径,不包含 API Key;该变量属于运行 DSH 的进程,目标仍由该进程的 `DSH_HOME` 决定。导入仅在 Iris 没有供应商时执行,复制供应商基本信息和凭据,不修改来源文件、不合并覆盖已有供应商,也不迁移模型池或能力分配。之后应在工作台检查协议并重新分配模型,后续启动不再设置此变量。
|
|
111
|
+
|
|
112
|
+
变量未设置时不进行导入;相对路径会被拒绝。文件不可读、JSON 无效或没有有效供应商时会给出提示,不输出密钥。导入后的密钥写入 Iris 自己的配置文件,权限设为仅当前用户可读写;工作台和接口只返回密钥提示,不返回完整值。该操作不发送模型请求。
|
|
113
|
+
|
|
96
114
|
## 高级配置文件
|
|
97
115
|
|
|
98
116
|
一般应使用 Iris 工作台。只有在配置工作台尚未覆盖的协议或模型能力时,才需要直接编辑:
|
|
@@ -132,7 +150,7 @@ $DSH_HOME/iris/v1/providers.json
|
|
|
132
150
|
|
|
133
151
|
`assignments` 可以省略;省略后使用模型池顺序。包含特殊字符的 provider 或模型引用会经过 URL 编码,复杂引用建议在工作台中生成,不要手写。
|
|
134
152
|
|
|
135
|
-
API Key 以明文保存在宿主侧的 `providers.json`
|
|
153
|
+
API Key 以明文保存在宿主侧的 `providers.json` 中。POSIX 上 Iris 目录为 `0700`、文件为 `0600`,0.1.1 首次启动会收紧既有 Iris 树但不修改内容或跟随符号链接;Windows 的 mode 不能替代 ACL。状态接口和界面只返回掩码,不会显示完整 Key。不要把这个文件提交到版本库或发给他人。
|
|
136
154
|
|
|
137
155
|
## 文件输入
|
|
138
156
|
|
|
@@ -177,7 +195,13 @@ Iris 会在会话输入区显示运行中任务,并在 Iris 工作台保留最
|
|
|
177
195
|
| `$DSH_HOME/iris/v1/tasks.json` | 任务状态与附件索引 |
|
|
178
196
|
| `$DSH_HOME/iris/v1/providers.json` | 供应商、模型池和能力分配 |
|
|
179
197
|
|
|
180
|
-
|
|
198
|
+
音视频播放链接带随机令牌。默认只接受回环 Host。反向代理或 LAN 访问时,需要同时显式信任外部 Host,并按需设置媒体链接基址,例如:
|
|
199
|
+
|
|
200
|
+
```bash
|
|
201
|
+
IRIS_TRUSTED_HOSTS="dsh.example.com" DSH_WEB_BASE="https://dsh.example.com" dsh web
|
|
202
|
+
```
|
|
203
|
+
|
|
204
|
+
列表用逗号分隔,可填写 `host` 或精确的 `host:port`;反向代理应保留正确的 Host。`IRIS_TRUSTED_HOSTS` 只防止意外 Host 暴露,不验证用户身份。只要服务位于公网或不可信网络,就必须由反向代理或 DSH 部署层提供 HTTPS 和认证,不能把随机媒体令牌当作登录机制。
|
|
181
205
|
|
|
182
206
|
## 可选依赖
|
|
183
207
|
|
|
@@ -206,6 +230,7 @@ dsh plugin --profile web remove @mokuyoaxis/dsh-iris
|
|
|
206
230
|
- **设置中没有 Iris 工作台**:确认插件已加入 Web profile,并在安装后重启 DSH;可用 `dsh plugin --profile web list` 和 `--dump-config` 检查。
|
|
207
231
|
- **没有可用模型**:先确认供应商已启用且 Key 有效,再运行“发现模型”;不支持 `/models` 时手动添加。
|
|
208
232
|
- **OpenAI 兼容图片请求走错协议**:确认该供应商的 `mediaProtocol` 是 `openai-images`。
|
|
233
|
+
- **反向代理访问返回 `untrusted host`**:把浏览器实际使用的域名或 `host:port` 加入 `IRIS_TRUSTED_HOSTS`,保留 Host,并确认代理层已经启用认证。
|
|
209
234
|
- **看图可用但 Iris 模型池显示为空**:Agent 视觉工具可以回退到 DSH 全局视觉模型;需要在工作台操作时仍建议配置 Iris 的 `vision` 模型。
|
|
210
235
|
- **视频摘要提示 ffmpeg 不可用**:安装系统级 `ffmpeg` 和 `ffprobe`,并确保 DSH 进程的 PATH 能找到它们。
|
|
211
236
|
- **本地文件不存在**:这里的“本地”指 DSH 宿主,不一定是浏览器所在设备;优先用“上传文件”。
|