@lynn123411/dsh-web-auth-url 0.1.1 → 0.1.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/package.json +9 -4
- package/scripts/smoke-host.mjs +177 -106
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@lynn123411/dsh-web-auth-url",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.2",
|
|
4
4
|
"description": "把当前 Web 实例的带 token 访问地址交给模型与 shell:托管 DSH_WEB_AUTH_URL(每次 shell 调用即时解析,跟随重启后的 token 轮换),并贡献一段紧随 web-surface 的提示词说明 cookie jar 用法,secret 不进提示词与 transcript。",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "lib/index.js",
|
|
@@ -34,18 +34,23 @@
|
|
|
34
34
|
"check": "node scripts/smoke-host.mjs"
|
|
35
35
|
},
|
|
36
36
|
"dependencies": {
|
|
37
|
-
"@deepseek-ai/schemastery": "^3.18.
|
|
37
|
+
"@deepseek-ai/schemastery": "^3.18.4"
|
|
38
38
|
},
|
|
39
39
|
"peerDependencies": {
|
|
40
|
-
"@deepseek-ai/cordis": "^4.0.
|
|
40
|
+
"@deepseek-ai/cordis": "^4.0.4"
|
|
41
41
|
},
|
|
42
42
|
"peerDependenciesMeta": {
|
|
43
43
|
"@deepseek-ai/cordis": {
|
|
44
44
|
"optional": true
|
|
45
45
|
}
|
|
46
46
|
},
|
|
47
|
+
"devDependencies": {
|
|
48
|
+
"@deepseek-ai/cordis": "4.0.4",
|
|
49
|
+
"@deepseek-ai/dsh-shell-env": "0.1.7-rc.2",
|
|
50
|
+
"@deepseek-ai/dsh-system-prompt": "0.1.7-rc.2"
|
|
51
|
+
},
|
|
47
52
|
"engines": {
|
|
48
|
-
"dsh": "0.1.
|
|
53
|
+
"dsh": "0.1.7-rc.2",
|
|
49
54
|
"node": "^22.19 || >=24"
|
|
50
55
|
},
|
|
51
56
|
"publishConfig": {
|
package/scripts/smoke-host.mjs
CHANGED
|
@@ -1,32 +1,36 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
3
|
-
*
|
|
2
|
+
* 宿主半边自检:把两个承重点放在 **真实的 0.1.7-rc.2 内核服务** 上跑,而不是桩件——
|
|
3
|
+
* 这两条断言的全部意义就是「DSH 换了实现,本插件立刻挂」:
|
|
4
4
|
*
|
|
5
|
-
*
|
|
5
|
+
* 1. 托管环境变量:真实 `ctx.shellEnv`(@deepseek-ai/dsh-shell-env)的
|
|
6
|
+
* `collect()` 每次调用都重新执行本插件的 `resolve`,所以 token 轮换后
|
|
7
|
+
* 下一次 shell 调用拿到新地址。桩件只能证明我们自己的闭包新鲜,证不了这一点。
|
|
8
|
+
* 2. 段落锚点:真实 `ctx.systemPrompt`(@deepseek-ai/dsh-system-prompt)里
|
|
9
|
+
* `app:web-auth-url` 必须紧跟 `app:web-surface`。`getSectionOrder('WEB_SURFACE')`
|
|
10
|
+
* 返回 undefined 时本插件算出的 order 是 NaN,真实 `section()` 会直接抛错。
|
|
11
|
+
*
|
|
12
|
+
* 两个内核包是本插件的 devDependencies(与 engines.dsh 同版本),因此自检不需要
|
|
13
|
+
* 指向任何 DSH 安装副本,`pnpm install` 后即可离线跑。文件末尾另有一段守卫
|
|
14
|
+
* 反例:cordis 对未声明服务的属性读取真的抛错,上面的「用 ctx.inject 拿服务」
|
|
15
|
+
* 才不是装饰。
|
|
16
|
+
*
|
|
17
|
+
* 跑法:pnpm test / node scripts/smoke-host.mjs
|
|
6
18
|
*/
|
|
7
19
|
|
|
8
20
|
import assert from 'node:assert/strict';
|
|
9
|
-
import {
|
|
10
|
-
import { existsSync } from 'node:fs';
|
|
11
|
-
import {
|
|
12
|
-
import { join } from 'node:path';
|
|
13
|
-
import {
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
const schemasteryEntry = join(dshHome, 'profiles', 'node_modules', '@deepseek-ai', 'schemastery', 'lib', 'index.mjs');
|
|
19
|
-
if (!existsSync(schemasteryEntry)) {
|
|
20
|
-
throw new Error(`schemastery 未找到:${schemasteryEntry}(把 DSH_HOME 指向 DSH 主目录后重试)`);
|
|
21
|
-
}
|
|
22
|
-
const schemasteryUrl = pathToFileURL(schemasteryEntry).href;
|
|
23
|
-
registerHooks({
|
|
24
|
-
resolve(specifier, context, nextResolve) {
|
|
25
|
-
if (specifier === '@deepseek-ai/schemastery') return { url: schemasteryUrl, shortCircuit: true };
|
|
26
|
-
return nextResolve(specifier, context);
|
|
27
|
-
},
|
|
28
|
-
});
|
|
21
|
+
import { createRequire } from 'node:module';
|
|
22
|
+
import { existsSync, readFileSync } from 'node:fs';
|
|
23
|
+
import { tmpdir } from 'node:os';
|
|
24
|
+
import { dirname, join } from 'node:path';
|
|
25
|
+
import { fileURLToPath } from 'node:url';
|
|
26
|
+
|
|
27
|
+
import { Context } from '@deepseek-ai/cordis';
|
|
28
|
+
import SystemPrompt, { renderPrompt } from '@deepseek-ai/dsh-system-prompt';
|
|
29
|
+
import * as ShellEnv from '@deepseek-ai/dsh-shell-env';
|
|
29
30
|
|
|
31
|
+
const require = createRequire(import.meta.url);
|
|
32
|
+
const here = dirname(fileURLToPath(import.meta.url));
|
|
33
|
+
const manifest = JSON.parse(readFileSync(join(here, '..', 'package.json'), 'utf8'));
|
|
30
34
|
const { apply, Config, inject, name } = await import('../lib/index.js');
|
|
31
35
|
|
|
32
36
|
const PORT = 3080;
|
|
@@ -34,117 +38,184 @@ const TOKEN = 'launch-token-example';
|
|
|
34
38
|
const CLEAN_URL = `http://127.0.0.1:${PORT}`;
|
|
35
39
|
const AUTH_URL = `${CLEAN_URL}/?token=${TOKEN}`;
|
|
36
40
|
|
|
37
|
-
/**
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
*/
|
|
43
|
-
function
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
get(target, property, receiver) {
|
|
55
|
-
if (typeof property === 'symbol' || property in target) return Reflect.get(target, property, receiver);
|
|
56
|
-
if (!declared.has(property)) throw new Error(`cannot get property "${property}" without inject`);
|
|
57
|
-
return services[property];
|
|
58
|
-
},
|
|
59
|
-
});
|
|
41
|
+
/** 本插件贡献的段落名。 */
|
|
42
|
+
const SECTION_NAME = 'app:web-auth-url';
|
|
43
|
+
/** 0.1.7-rc.2 `SECTION_ORDERS.WEB_SURFACE` 的实际取值,锚点漂了必须当场失败。 */
|
|
44
|
+
const ANCHOR_ORDER = 10100;
|
|
45
|
+
|
|
46
|
+
/** 读一个已安装包的真实版本号(沿解析出的入口向上找同名 package.json)。 */
|
|
47
|
+
function installedVersion(pkg) {
|
|
48
|
+
let dir = dirname(require.resolve(pkg));
|
|
49
|
+
while (dirname(dir) !== dir) {
|
|
50
|
+
const candidate = join(dir, 'package.json');
|
|
51
|
+
if (existsSync(candidate)) {
|
|
52
|
+
const found = JSON.parse(readFileSync(candidate, 'utf8'));
|
|
53
|
+
if (found.name === pkg) return found.version;
|
|
54
|
+
}
|
|
55
|
+
dir = dirname(dir);
|
|
56
|
+
}
|
|
57
|
+
throw new Error(`已安装包 ${pkg} 的 package.json 未找到`);
|
|
60
58
|
}
|
|
61
59
|
|
|
62
|
-
/**
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
60
|
+
/**
|
|
61
|
+
* 装配一套真实的宿主树:真 systemPrompt + 真 shellEnv,web 侧用最小桩供给
|
|
62
|
+
* `webServer.port` 与 `connection.authenticatedUrl`(dsh-web-app 依赖同样的两个面)。
|
|
63
|
+
* @param {{token?: string, web?: boolean}} [options] - token 初值与是否提供 web 部署。
|
|
64
|
+
* @returns {Promise<{ctx: object, state: {token: string}}>} 上下文与可轮换的 token 状态。
|
|
65
|
+
*/
|
|
66
|
+
async function host({ token = TOKEN, web = true } = {}) {
|
|
67
|
+
const ctx = new Context();
|
|
68
|
+
await ctx.plugin(SystemPrompt, {});
|
|
69
|
+
await ctx.plugin(ShellEnv, { dshHome: join(tmpdir(), 'dsh-web-auth-url-smoke') });
|
|
70
|
+
const state = { token };
|
|
71
|
+
if (web) {
|
|
72
|
+
ctx.provide('webServer', { port: PORT });
|
|
73
|
+
ctx.provide('connection', { authenticatedUrl: (baseUrl) => `${baseUrl}/?token=${state.token}` });
|
|
74
|
+
}
|
|
75
|
+
return { ctx, state };
|
|
70
76
|
}
|
|
71
77
|
|
|
72
|
-
/**
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
captured.section = section;
|
|
81
|
-
return () => {};
|
|
82
|
-
};
|
|
83
|
-
apply(makeContext(services), config);
|
|
84
|
-
return { captured, services };
|
|
78
|
+
/**
|
|
79
|
+
* 把本插件应用到真实上下文。依赖齐备时 `ctx.inject` 的登记回调同步执行;
|
|
80
|
+
* 这里仍 await 一次 fiber,是为了让「将来改成异步注册」变成失败而不是静默漏断言。
|
|
81
|
+
*/
|
|
82
|
+
async function mount(ctx, config) {
|
|
83
|
+
apply(ctx, config);
|
|
84
|
+
await ctx.fiber.await();
|
|
85
|
+
return ctx;
|
|
85
86
|
}
|
|
86
87
|
|
|
87
|
-
// ---
|
|
88
|
+
// --- 插件身份与清单口径 ---------------------------------------------------
|
|
88
89
|
assert.equal(name, 'dsh-web-auth-url');
|
|
89
90
|
assert.deepEqual(inject, [], '插件必须声明零硬依赖,web 缺失时靠 ctx.get 降级');
|
|
90
91
|
assert.equal(Config({}).prompt, 'env', 'prompt 默认必须是 env(token 不进提示词)');
|
|
91
92
|
assert.equal(Config({ prompt: 'inline' }).prompt, 'inline');
|
|
93
|
+
assert.equal(Config({ prompt: 'off' }).prompt, 'off');
|
|
92
94
|
assert.throws(() => Config({ prompt: 'nope' }), '非法 prompt 必须被 schema 拒绝');
|
|
93
95
|
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
assert.
|
|
101
|
-
|
|
96
|
+
assert.equal(manifest.engines.dsh, '0.1.7-rc.2', 'engines.dsh 必须是精确的目标版本');
|
|
97
|
+
assert.equal(manifest.peerDependencies['@deepseek-ai/cordis'], '^4.0.4');
|
|
98
|
+
assert.equal(installedVersion('@deepseek-ai/cordis'), '4.0.4', 'cordis 必须对齐安装版本');
|
|
99
|
+
assert.equal(manifest.dependencies['@deepseek-ai/schemastery'], '^3.18.4');
|
|
100
|
+
assert.equal(installedVersion('@deepseek-ai/schemastery'), '3.18.4', 'schemastery 必须对齐安装版本');
|
|
101
|
+
for (const pkg of ['@deepseek-ai/dsh-system-prompt', '@deepseek-ai/dsh-shell-env']) {
|
|
102
|
+
assert.equal(
|
|
103
|
+
installedVersion(pkg),
|
|
104
|
+
manifest.engines.dsh,
|
|
105
|
+
`${pkg} 的 devDependency 必须与 engines.dsh 同版本,否则自检跑的不是目标版本`,
|
|
106
|
+
);
|
|
102
107
|
}
|
|
103
108
|
|
|
104
|
-
// ---
|
|
109
|
+
// --- 承重点一:真 shellEnv,每次 collect 即时解析 --------------------------
|
|
105
110
|
{
|
|
106
|
-
const {
|
|
107
|
-
|
|
108
|
-
|
|
111
|
+
const { ctx, state } = await host();
|
|
112
|
+
await mount(ctx, { prompt: 'env' });
|
|
113
|
+
|
|
114
|
+
const first = ctx.shellEnv.collect({});
|
|
115
|
+
assert.equal(first.DSH_WEB_AUTH_URL, AUTH_URL, '真实 shellEnv.collect 必须给出带 token 的当前地址');
|
|
116
|
+
|
|
117
|
+
state.token = 'rotated-token';
|
|
118
|
+
const second = ctx.shellEnv.collect({});
|
|
119
|
+
assert.equal(
|
|
120
|
+
second.DSH_WEB_AUTH_URL,
|
|
121
|
+
`${CLEAN_URL}/?token=rotated-token`,
|
|
122
|
+
'token 轮换后下一次 collect 必须重新解析(每次 shell 调用即时解析)',
|
|
123
|
+
);
|
|
124
|
+
|
|
125
|
+
const declared = ctx.shellEnv.list().find((variable) => variable.key === 'DSH_WEB_AUTH_URL');
|
|
126
|
+
assert.equal(declared?.contributor, 'dsh-web-auth-url', '变量必须以本插件名登记,真实注册表校验名字与键唯一性');
|
|
127
|
+
assert.ok(declared.description.trim().length > 0, '登记的变量必须有描述(真实注册表会拒绝空描述)');
|
|
109
128
|
}
|
|
110
129
|
|
|
111
|
-
// ---
|
|
130
|
+
// --- 承重点二:真 systemPrompt,段落紧贴 app:web-surface --------------------
|
|
112
131
|
{
|
|
113
|
-
const
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
const
|
|
117
|
-
assert.
|
|
118
|
-
|
|
132
|
+
const { ctx } = await host();
|
|
133
|
+
await mount(ctx, { prompt: 'env' });
|
|
134
|
+
|
|
135
|
+
const anchorOrder = ctx.systemPrompt.getSectionOrder('WEB_SURFACE');
|
|
136
|
+
assert.ok(
|
|
137
|
+
Number.isFinite(anchorOrder),
|
|
138
|
+
"真实 systemPrompt 必须仍认识 'WEB_SURFACE' 位置——返回 undefined 时本插件算出 NaN,真实 section() 会抛错",
|
|
139
|
+
);
|
|
140
|
+
assert.equal(anchorOrder, ANCHOR_ORDER, 'WEB_SURFACE 位置值变了,+10 的紧邻关系必须重新核对');
|
|
141
|
+
|
|
142
|
+
/**
|
|
143
|
+
* 复刻 dsh-web-app 的锚点:它在 0.1.7-rc.2 以
|
|
144
|
+
* `section({ name: 'app:web-surface', order: getSectionOrder('WEB_SURFACE') })` 注册
|
|
145
|
+
* (安装副本 `lib/index.js` 第 180-184 行)。它不在本插件的依赖里,所以只能照同样的
|
|
146
|
+
* 两件事复刻一个再断言紧邻;「这个名字对得上 dsh-web-app」靠读那一处源码保证,不是
|
|
147
|
+
* 这条断言证明的。名字与期望值都写死成字面量,改任何一处都得同时面对另一处。
|
|
148
|
+
*/
|
|
149
|
+
ctx.systemPrompt.section({ name: 'app:web-surface', order: anchorOrder, text: () => 'web surface' });
|
|
150
|
+
const assembly = await ctx.systemPrompt.assemble({});
|
|
151
|
+
const names = assembly.sections.map((section) => section.name);
|
|
152
|
+
const mine = names.indexOf(SECTION_NAME);
|
|
153
|
+
assert.notEqual(mine, -1, '段落必须注册进真实 systemPrompt');
|
|
154
|
+
assert.deepEqual(
|
|
155
|
+
names.slice(mine - 1, mine + 1),
|
|
156
|
+
['app:web-surface', SECTION_NAME],
|
|
157
|
+
'本段落必须紧跟 app:web-surface',
|
|
158
|
+
);
|
|
159
|
+
|
|
160
|
+
const text = assembly.sections[mine].text;
|
|
161
|
+
assert.ok(text.includes('DSH_WEB_AUTH_URL'), 'env 档必须报出变量名');
|
|
162
|
+
assert.ok(!text.includes(TOKEN), 'env 档绝不能把 token 写进段落');
|
|
163
|
+
|
|
164
|
+
const rendered = renderPrompt(assembly);
|
|
165
|
+
assert.ok(rendered.includes('DSH_WEB_AUTH_URL'), 'env 档渲染后的系统提示词必须交代变量名');
|
|
166
|
+
assert.ok(!rendered.includes(TOKEN), 'env 档渲染后的系统提示词里绝不能出现 token');
|
|
167
|
+
assert.ok(
|
|
168
|
+
text.includes('401') && text.includes('303') && text.includes('$JAR'),
|
|
169
|
+
'env 档必须交代 401 / 303 / cookie jar 用法',
|
|
170
|
+
);
|
|
119
171
|
}
|
|
120
172
|
|
|
121
|
-
// ---
|
|
173
|
+
// --- inline / off 两档 ----------------------------------------------------
|
|
122
174
|
{
|
|
123
|
-
const {
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
assert.ok(
|
|
128
|
-
assert.
|
|
129
|
-
assert.ok(text.includes('401') && text.includes('303') && text.includes('$JAR'), 'env 模式必须交代 401 / 303 / cookie jar 用法');
|
|
175
|
+
const { ctx } = await host();
|
|
176
|
+
await mount(ctx, { prompt: 'inline' });
|
|
177
|
+
const rendered = renderPrompt(await ctx.systemPrompt.assemble({}));
|
|
178
|
+
assert.ok(rendered.includes(AUTH_URL), 'inline 档必须把带 token 的地址写进段落');
|
|
179
|
+
assert.ok(!rendered.includes('DSH_WEB_AUTH_URL'), 'inline 档不再报变量名');
|
|
180
|
+
assert.equal(ctx.shellEnv.collect({}).DSH_WEB_AUTH_URL, AUTH_URL, 'inline 档仍须托管环境变量');
|
|
130
181
|
}
|
|
131
182
|
{
|
|
132
|
-
const {
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
assert.ok(!
|
|
183
|
+
const { ctx } = await host();
|
|
184
|
+
await mount(ctx, { prompt: 'off' });
|
|
185
|
+
const sections = (await ctx.systemPrompt.assemble({})).sections;
|
|
186
|
+
assert.ok(!sections.some((section) => section.name === SECTION_NAME), 'off 档不得注册段落');
|
|
187
|
+
assert.equal(ctx.shellEnv.collect({}).DSH_WEB_AUTH_URL, AUTH_URL, 'off 档仍须托管环境变量');
|
|
136
188
|
}
|
|
189
|
+
|
|
190
|
+
// --- 无 web 部署:注册照常、值与段落都为空 --------------------------------
|
|
137
191
|
{
|
|
138
|
-
const {
|
|
139
|
-
|
|
140
|
-
|
|
192
|
+
const { ctx } = await host({ web: false });
|
|
193
|
+
await mount(ctx, { prompt: 'env' });
|
|
194
|
+
|
|
195
|
+
assert.equal('DSH_WEB_AUTH_URL' in ctx.shellEnv.collect({}), false, '缺 connection 时不得注入该变量');
|
|
196
|
+
|
|
197
|
+
const assembly = await ctx.systemPrompt.assemble({});
|
|
198
|
+
const mine = assembly.sections.find((section) => section.name === SECTION_NAME);
|
|
199
|
+
assert.notEqual(mine, undefined, '段落注册不依赖 web 部署,照常完成');
|
|
200
|
+
assert.equal(mine.text, '', '缺 connection 时段落必须渲染成空串');
|
|
201
|
+
assert.ok(
|
|
202
|
+
!renderPrompt(assembly).includes('Shell access to this Web GUI'),
|
|
203
|
+
'空段落由真实 renderPrompt 过滤,系统提示词里不留痕迹',
|
|
204
|
+
);
|
|
141
205
|
}
|
|
142
206
|
|
|
143
|
-
// ---
|
|
207
|
+
// --- 守卫反例:cordis 真的拒绝未声明服务的属性读取 ------------------------
|
|
144
208
|
{
|
|
145
|
-
const ctx =
|
|
146
|
-
|
|
209
|
+
const ctx = new Context();
|
|
210
|
+
await ctx.plugin(ShellEnv, { dshHome: join(tmpdir(), 'dsh-web-auth-url-smoke') });
|
|
211
|
+
await assert.rejects(
|
|
212
|
+
async () => {
|
|
213
|
+
await ctx.plugin({ name: 'guarded', apply: (guardedCtx) => void guardedCtx.shellEnv });
|
|
214
|
+
},
|
|
215
|
+
/cannot get property "shellEnv" without inject/,
|
|
216
|
+
'未声明服务必须抛错,否则本插件用 ctx.inject([...]) 取服务就不是必需的了',
|
|
217
|
+
);
|
|
147
218
|
assert.equal(ctx.get('shellEnv') !== undefined, true, 'ctx.get 不抛错,可选能力走它');
|
|
148
219
|
}
|
|
149
220
|
|
|
150
|
-
console.log('smoke-host: ok');
|
|
221
|
+
console.log('smoke-host: ok');
|