@lynn123411/dsh-web-auth-url 0.1.0 → 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/README.md CHANGED
@@ -16,4 +16,3 @@ DSH Web GUI 的访问鉴权补位:把当前实例**带 token 的访问地址**
16
16
  dsh plugin --profile web add @lynn123411/dsh-web-auth-url
17
17
  ```
18
18
 
19
- 本地开发按仓库规则用 `link:`——`cd ~/.dsh/profiles/web && pnpm add "link:/Users/tny/Desktop/work/my-dsh/plugins/dsh-web-auth-url" --offline`,并在 profile `package.json` 的 `dsh.profile.bundles` 里登记本包(`dsh plugin add` 会自动登记,手工 link 不会)。宿主半边只在 DSH 启动时装载,改完须重启实例;自检 `npm test` 跑 `scripts/smoke-host.mjs`。
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lynn123411/dsh-web-auth-url",
3
- "version": "0.1.0",
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,17 +34,23 @@
34
34
  "check": "node scripts/smoke-host.mjs"
35
35
  },
36
36
  "dependencies": {
37
- "@deepseek-ai/schemastery": "^3.18.2"
37
+ "@deepseek-ai/schemastery": "^3.18.4"
38
38
  },
39
39
  "peerDependencies": {
40
- "@deepseek-ai/cordis": "^4.0.2"
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": {
53
+ "dsh": "0.1.7-rc.2",
48
54
  "node": "^22.19 || >=24"
49
55
  },
50
56
  "publishConfig": {
@@ -1,129 +1,221 @@
1
1
  /**
2
- * 宿主半边装配自检:用带守卫的假上下文真跑一遍 apply(),断言登记形状、
3
- * 即时解析的地址,以及三种 prompt 模式各自的段落文本。
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 { apply, Config, inject, name } from '../lib/index.js';
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';
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'));
34
+ const { apply, Config, inject, name } = await import('../lib/index.js');
10
35
 
11
36
  const PORT = 3080;
12
37
  const TOKEN = 'launch-token-example';
13
38
  const CLEAN_URL = `http://127.0.0.1:${PORT}`;
14
39
  const AUTH_URL = `${CLEAN_URL}/?token=${TOKEN}`;
15
40
 
16
- /**
17
- * 造一个假上下文。
18
- * @param services - 树上可读到的服务。
19
- * @param declared - 本上下文已 inject 的服务名。
20
- * @returns 读未声明服务即抛 cordis 同款错误的上下文。
21
- */
22
- function makeContext(services, declared = new Set()) {
23
- const base = {
24
- get: (key) => services[key],
25
- inject: (names, callback) => {
26
- for (const key of names) {
27
- if (!(key in services)) throw new Error(`fake tree is missing service "${key}"`);
28
- }
29
- callback(makeContext(services, new Set(names)));
30
- },
31
- };
32
- return new Proxy(base, {
33
- get(target, property, receiver) {
34
- if (typeof property === 'symbol' || property in target) return Reflect.get(target, property, receiver);
35
- if (!declared.has(property)) throw new Error(`cannot get property "${property}" without inject`);
36
- return services[property];
37
- },
38
- });
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 未找到`);
39
58
  }
40
59
 
41
- /** 一套带 web 部署的服务桩。 */
42
- function webServices() {
43
- return {
44
- webServer: { port: PORT },
45
- connection: { authenticatedUrl: (baseUrl) => `${baseUrl}/?token=${TOKEN}` },
46
- shellEnv: { register: () => () => {} },
47
- systemPrompt: { section: () => () => {}, getSectionOrder: (order) => (order === 'WEB_SURFACE' ? 10100 : Number.NaN) },
48
- };
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 };
49
76
  }
50
77
 
51
- /** 捕获一次 apply() 的登记结果。 */
52
- function load(config, services = webServices()) {
53
- const captured = { contributor: undefined, section: undefined };
54
- services.shellEnv.register = (contributor) => {
55
- captured.contributor = contributor;
56
- return () => {};
57
- };
58
- services.systemPrompt.section = (section) => {
59
- captured.section = section;
60
- return () => {};
61
- };
62
- apply(makeContext(services), config);
63
- 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;
64
86
  }
65
87
 
66
- // --- 插件身份 -------------------------------------------------------------
88
+ // --- 插件身份与清单口径 ---------------------------------------------------
67
89
  assert.equal(name, 'dsh-web-auth-url');
68
90
  assert.deepEqual(inject, [], '插件必须声明零硬依赖,web 缺失时靠 ctx.get 降级');
69
91
  assert.equal(Config({}).prompt, 'env', 'prompt 默认必须是 env(token 不进提示词)');
70
92
  assert.equal(Config({ prompt: 'inline' }).prompt, 'inline');
93
+ assert.equal(Config({ prompt: 'off' }).prompt, 'off');
71
94
  assert.throws(() => Config({ prompt: 'nope' }), '非法 prompt 必须被 schema 拒绝');
72
95
 
73
- // --- 环境变量 -------------------------------------------------------------
74
- {
75
- const { captured } = load({ prompt: 'env' });
76
- const contributor = captured.contributor;
77
- assert.equal(contributor.name, 'dsh-web-auth-url');
78
- assert.deepEqual(Object.keys(contributor.variables), ['DSH_WEB_AUTH_URL']);
79
- assert.ok(contributor.variables.DSH_WEB_AUTH_URL.description.trim().length > 0, '登记的变量必须有描述');
80
- assert.deepEqual(contributor.resolve({}), { DSH_WEB_AUTH_URL: AUTH_URL }, 'resolve 必须给出带 token 的当前地址');
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
+ );
81
107
  }
82
108
 
83
- // --- token 轮换:每次 resolve 重新解析 -------------------------------------
109
+ // --- 承重点一:真 shellEnv,每次 collect 即时解析 --------------------------
84
110
  {
85
- const { captured, services } = load({ prompt: 'env' });
86
- services.connection.authenticatedUrl = (baseUrl) => `${baseUrl}/?token=rotated`;
87
- assert.deepEqual(captured.contributor.resolve({}), { DSH_WEB_AUTH_URL: `${CLEAN_URL}/?token=rotated` });
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, '登记的变量必须有描述(真实注册表会拒绝空描述)');
88
128
  }
89
129
 
90
- // --- 无 web 部署:不报错、不贡献值 -----------------------------------------
130
+ // --- 承重点二:真 systemPrompt,段落紧贴 app:web-surface --------------------
91
131
  {
92
- const services = webServices();
93
- delete services.connection;
94
- delete services.webServer;
95
- const { captured } = load({ prompt: 'env' }, services);
96
- assert.deepEqual(captured.contributor.resolve({}), {}, '缺 connection 时 resolve 必须返回空');
97
- assert.equal(captured.section.text(), '', '缺 connection 时段落必须渲染成空串');
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
+ );
98
171
  }
99
172
 
100
- // --- 提示词段落 -----------------------------------------------------------
173
+ // --- inline / off 两档 ----------------------------------------------------
101
174
  {
102
- const { captured } = load({ prompt: 'env' });
103
- assert.equal(captured.section.name, 'app:web-auth-url');
104
- assert.equal(captured.section.order, 10110, '段落必须紧跟 WEB_SURFACE(10100)');
105
- const text = captured.section.text();
106
- assert.ok(text.includes('DSH_WEB_AUTH_URL'), 'env 模式必须报出变量名');
107
- assert.ok(!text.includes(TOKEN), 'env 模式绝不能把 token 写进提示词');
108
- 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 档仍须托管环境变量');
109
181
  }
110
182
  {
111
- const { captured } = load({ prompt: 'inline' });
112
- const text = captured.section.text();
113
- assert.ok(text.includes(AUTH_URL), 'inline 模式必须把带 token 的地址写进段落');
114
- assert.ok(!text.includes('DSH_WEB_AUTH_URL'), 'inline 模式不再报变量名');
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 档仍须托管环境变量');
115
188
  }
189
+
190
+ // --- 无 web 部署:注册照常、值与段落都为空 --------------------------------
116
191
  {
117
- const { captured } = load({ prompt: 'off' });
118
- assert.equal(captured.section, undefined, 'off 模式不得注册段落');
119
- assert.ok(captured.contributor !== undefined, 'off 模式仍须登记环境变量');
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
+ );
120
205
  }
121
206
 
122
- // --- 守卫自身的反例断言(守卫失效即失败) ---------------------------------
207
+ // --- 守卫反例:cordis 真的拒绝未声明服务的属性读取 ------------------------
123
208
  {
124
- const ctx = makeContext(webServices());
125
- assert.throws(() => ctx.shellEnv, /without inject/, '读未声明服务必须抛错,否则上面的断言全是空转');
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
+ );
126
218
  assert.equal(ctx.get('shellEnv') !== undefined, true, 'ctx.get 不抛错,可选能力走它');
127
219
  }
128
220
 
129
- console.log('smoke-host: ok');
221
+ console.log('smoke-host: ok');
package/LICENSE DELETED
@@ -1,21 +0,0 @@
1
- MIT License
2
-
3
- Copyright (c) 2026 lynn123411
4
-
5
- Permission is hereby granted, free of charge, to any person obtaining a copy
6
- of this software and associated documentation files (the "Software"), to deal
7
- in the Software without restriction, including without limitation the rights
8
- to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
- copies of the Software, and to permit persons to whom the Software is
10
- furnished to do so, subject to the following conditions:
11
-
12
- The above copyright notice and this permission notice shall be included in all
13
- copies or substantial portions of the Software.
14
-
15
- THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
- IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
- FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
- AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
- LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
- OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
- SOFTWARE.