@lynn123411/dsh-web-auth-url 0.1.1 → 0.1.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/README.md CHANGED
@@ -5,10 +5,11 @@ DSH Web GUI 的访问鉴权补位:把当前实例**带 token 的访问地址**
5
5
  ## 特性
6
6
 
7
7
  - **`DSH_WEB_AUTH_URL` 托管环境变量**:每次 shell 调用即时解析的带 token 回环地址(`http://127.0.0.1:<port>/?token=…`),DSH 重启换 token 后自动跟随;不缓存、不落盘。
8
- - **提示词段落 `app:web-auth-url`**:紧随 `app:web-surface`(order + 10)贡献一段说明——只报变量名、不报值,并交代必需用法:第一次请求 `curl -c "$JAR" -b "$JAR" "$DSH_WEB_AUTH_URL"` 拿到 303 并种下 cookie,之后复用同一个 jar 即已授权。
9
- - **`prompt` 三档**:`env`(默认)只报变量名,token 不进提示词与 transcript;`inline` 把带 token 的地址直接写进段落(地址每次装配即时解析,重启后不过期,但 token 会随请求发给模型服务商并落盘进 session);`off` 只保留环境变量。
8
+ - **提示词段落 `app:web-auth-url`**:紧随 `app:web-surface`(order + 10)贡献一段说明——只报变量名、不报值,并交代必需用法:第一次请求 `curl -s -c "$JAR" -b "$JAR" -o /dev/null -w '%{http_code}' "$DSH_WEB_AUTH_URL"` 拿到 303 并种下 cookie,**之后复用同一个 jar、不再带 token**(token 只对 `GET /` 有效,其余路径带它一律 401)。
9
+ - **`prompt` 三档**:`env`(默认)只报变量名,token 不进提示词与 transcript;`inline` 把带 token 的地址直接写进段落(地址每次装配即时解析,重启后不过期,但 token 会随请求发给模型服务商并落盘进 session);`off` 只保留环境变量。两档共用同一份句式,只有地址那处被替换。
10
10
  - **无 web 部署静默降级**:CLI / TUI / 缺 `connection` 的组合下注册照常完成,但 `resolve` 返回空、段落渲染成空串,而空段落由内核 `renderPrompt` 过滤,系统提示词里不留痕迹。
11
11
  - **不新增端口、不新增凭据**:token 取自已装载的 `connection` 服务,与启动时打印到 stdout 的是同一个。
12
+ - **`selfDir()`**:本插件自身安装目录的真实路径(预留工具,当前无调用方)。开发副本是 `link:` 软链接且 `~/.dsh` 与仓库常在不同卷,`ctx.pluginPackages.packageOf(...)` 会返回链接路径;`selfDir()` 走 Node 解析器 + `realpath`,正常发布安装下两者相同。
12
13
 
13
14
  ## 安装
14
15
 
package/lib/index.js CHANGED
@@ -1,34 +1,31 @@
1
1
  /**
2
2
  * @lynn123411/dsh-web-auth-url — 宿主半区(node)。
3
3
  *
4
- * Web GUI 的每个请求都要过 connection 的浏览器会话鉴权,而鉴权的入口只有
5
- * 一个:带本进程 launch token 的根地址。干净地址 `http://127.0.0.1:<port>`
6
- * 一律 401,而且这是 `dsh-web-app` 有意为之——它把干净地址交给模型
7
- * (`app:web-surface` 段落)和 shell(`DSH_WEB_URL`),带 token 的地址只
8
- * 打印到 stdout、交给默认浏览器。
4
+ * connection 的浏览器会话鉴权只有一个入口:带本进程 launch token 的根地址;
5
+ * `dsh-web-app` 有意只把干净地址交给模型与 shell,带 token 的地址仅打印到
6
+ * stdout。于是模型自测这个 GUI 时第一枪必然 401。本插件补上这个缺口而不把
7
+ * secret 放进提示词或 transcript:托管 `DSH_WEB_AUTH_URL`(每次 shell 调用
8
+ * 即时解析,跟随重启后的 token 轮换),并紧随 `app:web-surface` 说明它的
9
+ * 用法——只报变量名,`prompt: 'inline'` 才把地址写进段落。
9
10
  *
10
- * 代价是模型想自测这个 GUI(抓页面、探路由、确认客户端插件有没有生效)时
11
- * 第一枪必然 401,只能翻启动器日志找 token。本插件补上这个缺口,同时不把
12
- * secret 放进提示词或 transcript:
13
- *
14
- * 1. `DSH_WEB_AUTH_URL` —— 每次 shell 调用**即时解析**的带 token 地址,
15
- * 因此 DSH 重启换 token 后自动跟随(静态字符串做不到这一点)。
16
- * 2. 紧随 `app:web-surface`(order + 10)的一段提示词,说明该地址存在,
17
- * 以及「第一次请求 303 换 cookie、之后复用同一个 jar」这个必需用法——
18
- * 只报变量名,不报值;`prompt: 'inline'` 才把地址直接写进段落。
19
- *
20
- * 没有 web 部署的宿主(CLI / TUI / 缺 connection 的组合)不是错误路径:注册
21
- * 照常完成,`resolve` 返回空、段落渲染成空串,而空段落会被 renderPrompt
22
- * 过滤掉,系统提示词里不留痕迹。
11
+ * 没有 web 部署的宿主不是错误路径:注册照常完成,值解析为空、段落渲染成空
12
+ * 串,而空段落会被 renderPrompt 过滤掉。
23
13
  *
24
14
  * @module dsh-web-auth-url
25
15
  */
26
16
 
27
17
  import z from '@deepseek-ai/schemastery'
28
18
 
19
+ import { realpathSync } from 'node:fs'
20
+ import { basename, dirname } from 'node:path'
21
+ import { fileURLToPath } from 'node:url'
22
+
29
23
  /** Cordis 插件名(= patch 行 id)。 */
30
24
  export const name = 'dsh-web-auth-url'
31
25
 
26
+ /** npm 包名:{@link selfDir} 靠它从本模块 URL 解析自身安装位置。 */
27
+ const PKG_NAME = '@lynn123411/dsh-web-auth-url'
28
+
32
29
  /** 本插件不硬依赖任何服务:web 服务缺失时静默降级,见模块头。 */
33
30
  export const inject = []
34
31
 
@@ -38,9 +35,6 @@ const LOOPBACK_HOST = '127.0.0.1'
38
35
  /** 本插件唯一登记的托管环境变量。 */
39
36
  const AUTH_URL_KEY = 'DSH_WEB_AUTH_URL'
40
37
 
41
- /** web-app 交给 shell 的干净地址,只在提示词里做对照。 */
42
- const CLEAN_URL_KEY = 'DSH_WEB_URL'
43
-
44
38
  /** shellEnv 贡献者名。 */
45
39
  const CONTRIBUTOR_NAME = 'dsh-web-auth-url'
46
40
 
@@ -50,6 +44,26 @@ const SECTION_NAME = 'app:web-auth-url'
50
44
  /** 段落排在 `app:web-surface` 之后,紧挨着它所补充的那段话。 */
51
45
  const SECTION_OFFSET = 10
52
46
 
47
+ /** 段落里出现 token 地址的位置(`env` 档保留字面量,`inline` 档换成实址)。 */
48
+ const URL_PLACEHOLDER = `$${AUTH_URL_KEY}`
49
+
50
+ /**
51
+ * 段落模板:两档共用一份句式,只替换地址,文案不会各自漂移。
52
+ *
53
+ * 首句借 `$DSH_WEB_URL` 把这段挂到 `app:web-surface` 刚报的那台 GUI 上(那
54
+ * 段不提 token / 401 / cookie,两段不重复);末句点明「token 只用于第一次
55
+ * 换 cookie」——connection 的 token 分支要求 `GET /` 且 pathname 恰为 `/`,
56
+ * 其余路径带 token 一律 401,不点破模型会继续在 `/api` 上挂 `?token=`。
57
+ *
58
+ * 地址一律加引号:token 是 base64url,理论上无外壳元字符,但两条分支用同
59
+ * 一种写法才不会有后加字符时的分叉。
60
+ */
61
+ const SECTION_TEMPLATE = [
62
+ 'The Web GUI at `$DSH_WEB_URL` needs auth: its URL and every request carry `$DSH_WEB_AUTH_URL`.',
63
+ 'First `curl -s -c "$JAR" -b "$JAR" -o /dev/null -w \'%{http_code}\' "$DSH_WEB_AUTH_URL"` answers 303',
64
+ 'and sets the session cookie; reuse that jar, not the token, afterwards.',
65
+ ].join(' ')
66
+
53
67
  /** 插件配置。 */
54
68
  export const Config = z.object({
55
69
  /**
@@ -79,7 +93,7 @@ function resolveAuthenticatedUrl(ctx) {
79
93
  }
80
94
 
81
95
  /**
82
- * 渲染提示词段落。
96
+ * 渲染提示词段落:一份模板,只换地址那处。
83
97
  * @param ctx - 装配用的上下文。
84
98
  * @param mode - 已解析的 `prompt` 配置。
85
99
  * @returns 段落文本;地址不可解析时返回空串(等于不贡献段落)。
@@ -87,18 +101,35 @@ function resolveAuthenticatedUrl(ctx) {
87
101
  function promptText(ctx, mode) {
88
102
  const url = resolveAuthenticatedUrl(ctx)
89
103
  if (url === undefined) return ''
90
- const inline = mode === 'inline'
91
- const address = inline
92
- ? url
93
- : `\`$${AUTH_URL_KEY}\` (\`$${CLEAN_URL_KEY}\` is the same origin without the token)`
94
- const target = inline ? url : `"$${AUTH_URL_KEY}"`
95
- return [
96
- 'Shell access to this Web GUI is authenticated: an unauthenticated request to the clean URL answers 401,',
97
- "because the browser session is a signed cookie minted from this process's launch token.",
98
- `The authenticated address is ${address}.`,
99
- `Exchange the token once and keep the jar — \`curl -s -c "$JAR" -b "$JAR" -o /dev/null -w '%{http_code}' ${target}\` answers 303 and sets the cookie;`,
100
- 'every later request through that jar is authorized.',
101
- ].join(' ')
104
+ return mode === 'inline'
105
+ ? SECTION_TEMPLATE.replaceAll(URL_PLACEHOLDER, url)
106
+ : SECTION_TEMPLATE
107
+ }
108
+
109
+ /**
110
+ * 本插件自身安装目录的真实路径(预留工具,当前没有调用方)。
111
+ *
112
+ * 用它,不要用 `ctx.pluginPackages.packageOf(...)`:`packageOf` 只把 specifier
113
+ * 解析成包名并读 `<node_modules>/<包名>/package.json`,返回的可能是**软链接
114
+ * 路径**——开发副本里 `~/.dsh/profiles/web/node_modules/@lynn123411/dsh-*` 正是
115
+ * `link:` 到仓库,跨卷时 Node 的 realpath 不穿透,dirname 出来的是仓库外的
116
+ * 路径。这里走 Node 自己的解析器再 realpath:正常发布安装下两者相同。
117
+ *
118
+ * 宿主半边运行时**不能**用 `import.meta.resolve`(Cordis 装载的模块拿不到该
119
+ * 能力),所以按包名从自己的 URL 解析,再退回自身目录。
120
+ *
121
+ * @returns 本包目录的绝对路径;解析失败时退回本模块所在目录。
122
+ */
123
+ export function selfDir() {
124
+ const fallback = dirname(fileURLToPath(import.meta.url))
125
+ try {
126
+ const entry = fileURLToPath(import.meta.resolve(`${PKG_NAME}`, import.meta.url))
127
+ let dir = dirname(entry)
128
+ if (basename(dir) === 'lib') dir = dirname(dir)
129
+ return realpathSync(dir)
130
+ } catch {
131
+ return fallback
132
+ }
102
133
  }
103
134
 
104
135
  /**
@@ -114,7 +145,7 @@ export function apply(ctx, config) {
114
145
  name: CONTRIBUTOR_NAME,
115
146
  variables: {
116
147
  [AUTH_URL_KEY]: {
117
- description: 'Canonical local URL of the DeepSeek Harness Web GUI serving this session, carrying this process launch token for the browser-session exchange.',
148
+ description: 'Authenticated URL of this Web GUI; GET it once with a cookie jar to log in, then reuse the jar.',
118
149
  },
119
150
  },
120
151
  resolve: () => {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lynn123411/dsh-web-auth-url",
3
- "version": "0.1.1",
3
+ "version": "0.1.3",
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.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": {
48
- "dsh": "0.1.6-alpha.2",
53
+ "dsh": "0.1.7-rc.2",
49
54
  "node": "^22.19 || >=24"
50
55
  },
51
56
  "publishConfig": {
@@ -1,150 +1,250 @@
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
+ * 文案断言只钉**承重的那几件事**(两档都带引号的 curl 管道、env 档不出现 token、
13
+ * 末句的「别再用 token」),不钉整句子:段落文案今后再压也不该动自检。
14
+ *
15
+ * 两个内核包是本插件的 devDependencies(与 engines.dsh 同版本),因此自检不需要
16
+ * 指向任何 DSH 安装副本,`pnpm install` 后即可离线跑。文件末尾另有一段守卫
17
+ * 反例:cordis 对未声明服务的属性读取真的抛错,上面的「用 ctx.inject 拿服务」
18
+ * 才不是装饰。
19
+ *
20
+ * 跑法:pnpm test / node scripts/smoke-host.mjs
6
21
  */
7
22
 
8
23
  import assert from 'node:assert/strict';
9
- import { registerHooks } from 'node:module';
10
- import { existsSync } from 'node:fs';
11
- import { homedir } from 'node:os';
12
- import { join } from 'node:path';
13
- import { pathToFileURL } from 'node:url';
14
-
15
- // lib/index.js 依赖 @deepseek-ai/schemastery;仓库检出里没有 node_modules,而 DSH 运行目录带一份。
16
- // 把该名字路由到运行目录里的真实实现——自检要真跑 schema 校验,不能用桩件假造它的行为。
17
- const dshHome = process.env.DSH_HOME ?? join(homedir(), '.dsh');
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
- });
24
+ import { createRequire } from 'node:module';
25
+ import { existsSync, readFileSync, realpathSync } from 'node:fs';
26
+ import { tmpdir } from 'node:os';
27
+ import { dirname, join } from 'node:path';
28
+ import { fileURLToPath } from 'node:url';
29
29
 
30
- const { apply, Config, inject, name } = await import('../lib/index.js');
30
+ import { Context } from '@deepseek-ai/cordis';
31
+ import SystemPrompt, { renderPrompt } from '@deepseek-ai/dsh-system-prompt';
32
+ import * as ShellEnv from '@deepseek-ai/dsh-shell-env';
33
+
34
+ const require = createRequire(import.meta.url);
35
+ const here = dirname(fileURLToPath(import.meta.url));
36
+ const packageDir = realpathSync(join(here, '..'));
37
+ const manifest = JSON.parse(readFileSync(join(packageDir, 'package.json'), 'utf8'));
38
+ const { apply, Config, inject, name, selfDir } = await import('../lib/index.js');
31
39
 
32
40
  const PORT = 3080;
33
41
  const TOKEN = 'launch-token-example';
34
42
  const CLEAN_URL = `http://127.0.0.1:${PORT}`;
35
43
  const AUTH_URL = `${CLEAN_URL}/?token=${TOKEN}`;
36
44
 
37
- /**
38
- * 造一个假上下文。
39
- * @param services - 树上可读到的服务。
40
- * @param declared - 本上下文已 inject 的服务名。
41
- * @returns 读未声明服务即抛 cordis 同款错误的上下文。
42
- */
43
- function makeContext(services, declared = new Set()) {
44
- const base = {
45
- get: (key) => services[key],
46
- inject: (names, callback) => {
47
- for (const key of names) {
48
- if (!(key in services)) throw new Error(`fake tree is missing service "${key}"`);
49
- }
50
- callback(makeContext(services, new Set(names)));
51
- },
52
- };
53
- return new Proxy(base, {
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
- });
45
+ /** 本插件贡献的段落名。 */
46
+ const SECTION_NAME = 'app:web-auth-url';
47
+ /** 0.1.7-rc.2 `SECTION_ORDERS.WEB_SURFACE` 的实际取值,锚点漂了必须当场失败。 */
48
+ const ANCHOR_ORDER = 10100;
49
+
50
+ /** 读一个已安装包的真实版本号(沿解析出的入口向上找同名 package.json)。 */
51
+ function installedVersion(pkg) {
52
+ let dir = dirname(require.resolve(pkg));
53
+ while (dirname(dir) !== dir) {
54
+ const candidate = join(dir, 'package.json');
55
+ if (existsSync(candidate)) {
56
+ const found = JSON.parse(readFileSync(candidate, 'utf8'));
57
+ if (found.name === pkg) return found.version;
58
+ }
59
+ dir = dirname(dir);
60
+ }
61
+ throw new Error(`已安装包 ${pkg} 的 package.json 未找到`);
60
62
  }
61
63
 
62
- /** 一套带 web 部署的服务桩。 */
63
- function webServices() {
64
- return {
65
- webServer: { port: PORT },
66
- connection: { authenticatedUrl: (baseUrl) => `${baseUrl}/?token=${TOKEN}` },
67
- shellEnv: { register: () => () => {} },
68
- systemPrompt: { section: () => () => {}, getSectionOrder: (order) => (order === 'WEB_SURFACE' ? 10100 : Number.NaN) },
69
- };
64
+ /**
65
+ * 装配一套真实的宿主树:真 systemPrompt + 真 shellEnv,web 侧用最小桩供给
66
+ * `webServer.port` 与 `connection.authenticatedUrl`(dsh-web-app 依赖同样的两个面)。
67
+ * @param {{token?: string, web?: boolean}} [options] - token 初值与是否提供 web 部署。
68
+ * @returns {Promise<{ctx: object, state: {token: string}}>} 上下文与可轮换的 token 状态。
69
+ */
70
+ async function host({ token = TOKEN, web = true } = {}) {
71
+ const ctx = new Context();
72
+ await ctx.plugin(SystemPrompt, {});
73
+ await ctx.plugin(ShellEnv, { dshHome: join(tmpdir(), 'dsh-web-auth-url-smoke') });
74
+ const state = { token };
75
+ if (web) {
76
+ ctx.provide('webServer', { port: PORT });
77
+ ctx.provide('connection', { authenticatedUrl: (baseUrl) => `${baseUrl}/?token=${state.token}` });
78
+ }
79
+ return { ctx, state };
70
80
  }
71
81
 
72
- /** 捕获一次 apply() 的登记结果。 */
73
- function load(config, services = webServices()) {
74
- const captured = { contributor: undefined, section: undefined };
75
- services.shellEnv.register = (contributor) => {
76
- captured.contributor = contributor;
77
- return () => {};
78
- };
79
- services.systemPrompt.section = (section) => {
80
- captured.section = section;
81
- return () => {};
82
- };
83
- apply(makeContext(services), config);
84
- return { captured, services };
82
+ /**
83
+ * 把本插件应用到真实上下文。依赖齐备时 `ctx.inject` 的登记回调同步执行;
84
+ * 这里仍 await 一次 fiber,是为了让「将来改成异步注册」变成失败而不是静默漏断言。
85
+ */
86
+ async function mount(ctx, config) {
87
+ apply(ctx, config);
88
+ await ctx.fiber.await();
89
+ return ctx;
85
90
  }
86
91
 
87
- // --- 插件身份 -------------------------------------------------------------
92
+ // --- 插件身份与清单口径 ---------------------------------------------------
88
93
  assert.equal(name, 'dsh-web-auth-url');
89
94
  assert.deepEqual(inject, [], '插件必须声明零硬依赖,web 缺失时靠 ctx.get 降级');
90
95
  assert.equal(Config({}).prompt, 'env', 'prompt 默认必须是 env(token 不进提示词)');
91
96
  assert.equal(Config({ prompt: 'inline' }).prompt, 'inline');
97
+ assert.equal(Config({ prompt: 'off' }).prompt, 'off');
92
98
  assert.throws(() => Config({ prompt: 'nope' }), '非法 prompt 必须被 schema 拒绝');
93
99
 
94
- // --- 环境变量 -------------------------------------------------------------
95
- {
96
- const { captured } = load({ prompt: 'env' });
97
- const contributor = captured.contributor;
98
- assert.equal(contributor.name, 'dsh-web-auth-url');
99
- assert.deepEqual(Object.keys(contributor.variables), ['DSH_WEB_AUTH_URL']);
100
- assert.ok(contributor.variables.DSH_WEB_AUTH_URL.description.trim().length > 0, '登记的变量必须有描述');
101
- assert.deepEqual(contributor.resolve({}), { DSH_WEB_AUTH_URL: AUTH_URL }, 'resolve 必须给出带 token 的当前地址');
100
+ // --- selfDir:本插件自身安装目录(预留工具) ------------------------------
101
+ // 钉「realpath 后的落点 = 本包目录」:开发副本下包名会被解析到 profiles 里的
102
+ // 链接路径,只有 realpath 之后才回到仓库。不 realpath 的实现在开发副本当场挂。
103
+ assert.equal(selfDir(), packageDir, 'selfDir 必须 realpath 回本包目录');
104
+ assert.equal(realpathSync(selfDir()), selfDir(), 'selfDir 必须已经是 realpath,调用方不必再处理链接');
105
+ assert.equal(existsSync(join(selfDir(), manifest.main)), true, 'selfDir 必须能拼出真实入口文件');
106
+ assert.equal(existsSync(join(selfDir(), 'cordis.patch.yml')), true, 'selfDir 必须能拼出随包文件');
107
+
108
+ assert.equal(manifest.engines.dsh, '0.1.7-rc.2', 'engines.dsh 必须是精确的目标版本');
109
+ assert.equal(manifest.peerDependencies['@deepseek-ai/cordis'], '^4.0.4');
110
+ assert.equal(installedVersion('@deepseek-ai/cordis'), '4.0.4', 'cordis 必须对齐安装版本');
111
+ assert.equal(manifest.dependencies['@deepseek-ai/schemastery'], '^3.18.4');
112
+ assert.equal(installedVersion('@deepseek-ai/schemastery'), '3.18.4', 'schemastery 必须对齐安装版本');
113
+ for (const pkg of ['@deepseek-ai/dsh-system-prompt', '@deepseek-ai/dsh-shell-env']) {
114
+ assert.equal(
115
+ installedVersion(pkg),
116
+ manifest.engines.dsh,
117
+ `${pkg} 的 devDependency 必须与 engines.dsh 同版本,否则自检跑的不是目标版本`,
118
+ );
102
119
  }
103
120
 
104
- // --- token 轮换:每次 resolve 重新解析 -------------------------------------
121
+ // --- 承重点一:真 shellEnv,每次 collect 即时解析 --------------------------
105
122
  {
106
- const { captured, services } = load({ prompt: 'env' });
107
- services.connection.authenticatedUrl = (baseUrl) => `${baseUrl}/?token=rotated`;
108
- assert.deepEqual(captured.contributor.resolve({}), { DSH_WEB_AUTH_URL: `${CLEAN_URL}/?token=rotated` });
123
+ const { ctx, state } = await host();
124
+ await mount(ctx, { prompt: 'env' });
125
+
126
+ const first = ctx.shellEnv.collect({});
127
+ assert.equal(first.DSH_WEB_AUTH_URL, AUTH_URL, '真实 shellEnv.collect 必须给出带 token 的当前地址');
128
+
129
+ state.token = 'rotated-token';
130
+ const second = ctx.shellEnv.collect({});
131
+ assert.equal(
132
+ second.DSH_WEB_AUTH_URL,
133
+ `${CLEAN_URL}/?token=rotated-token`,
134
+ 'token 轮换后下一次 collect 必须重新解析(每次 shell 调用即时解析)',
135
+ );
136
+
137
+ const declared = ctx.shellEnv.list().find((variable) => variable.key === 'DSH_WEB_AUTH_URL');
138
+ assert.equal(declared?.contributor, 'dsh-web-auth-url', '变量必须以本插件名登记,真实注册表校验名字与键唯一性');
139
+ assert.ok(declared.description.trim().length > 0, '登记的变量必须有描述(真实注册表会拒绝空描述)');
109
140
  }
110
141
 
111
- // --- 无 web 部署:不报错、不贡献值 -----------------------------------------
142
+ // --- 承重点二:真 systemPrompt,段落紧贴 app:web-surface --------------------
112
143
  {
113
- const services = webServices();
114
- delete services.connection;
115
- delete services.webServer;
116
- const { captured } = load({ prompt: 'env' }, services);
117
- assert.deepEqual(captured.contributor.resolve({}), {}, '缺 connection 时 resolve 必须返回空');
118
- assert.equal(captured.section.text(), '', '缺 connection 时段落必须渲染成空串');
144
+ const { ctx } = await host();
145
+ await mount(ctx, { prompt: 'env' });
146
+
147
+ const anchorOrder = ctx.systemPrompt.getSectionOrder('WEB_SURFACE');
148
+ assert.ok(
149
+ Number.isFinite(anchorOrder),
150
+ "真实 systemPrompt 必须仍认识 'WEB_SURFACE' 位置——返回 undefined 时本插件算出 NaN,真实 section() 会抛错",
151
+ );
152
+ assert.equal(anchorOrder, ANCHOR_ORDER, 'WEB_SURFACE 位置值变了,+10 的紧邻关系必须重新核对');
153
+
154
+ /**
155
+ * 复刻 dsh-web-app 的锚点:它在 0.1.7-rc.2 以
156
+ * `section({ name: 'app:web-surface', order: getSectionOrder('WEB_SURFACE') })` 注册
157
+ * (安装副本 `lib/index.js` 第 180-184 行)。它不在本插件的依赖里,所以只能照同样的
158
+ * 两件事复刻一个再断言紧邻;「这个名字对得上 dsh-web-app」靠读那一处源码保证,不是
159
+ * 这条断言证明的。名字与期望值都写死成字面量,改任何一处都得同时面对另一处。
160
+ */
161
+ ctx.systemPrompt.section({ name: 'app:web-surface', order: anchorOrder, text: () => 'web surface' });
162
+ const assembly = await ctx.systemPrompt.assemble({});
163
+ const names = assembly.sections.map((section) => section.name);
164
+ const mine = names.indexOf(SECTION_NAME);
165
+ assert.notEqual(mine, -1, '段落必须注册进真实 systemPrompt');
166
+ assert.deepEqual(
167
+ names.slice(mine - 1, mine + 1),
168
+ ['app:web-surface', SECTION_NAME],
169
+ '本段落必须紧跟 app:web-surface',
170
+ );
171
+
172
+ const text = assembly.sections[mine].text;
173
+ assert.ok(text.includes('DSH_WEB_AUTH_URL'), 'env 档必须报出变量名');
174
+ assert.ok(text.includes('DSH_WEB_URL'), '首句必须点到干净地址,才挂得上 app:web-surface 刚报的那台 GUI');
175
+ assert.ok(!text.includes(TOKEN), 'env 档绝不能把 token 写进段落');
176
+
177
+ const rendered = renderPrompt(assembly);
178
+ assert.ok(rendered.includes('DSH_WEB_AUTH_URL'), 'env 档渲染后的系统提示词必须交代变量名');
179
+ assert.ok(!rendered.includes(TOKEN), 'env 档渲染后的系统提示词里绝不能出现 token');
180
+ assert.ok(
181
+ text.includes('curl -s -c "$JAR" -b "$JAR" -o /dev/null -w \'%{http_code}\' "$DSH_WEB_AUTH_URL"'),
182
+ 'env 档必须给出带引号的 cookie jar 管道,且地址用变量引用的写法',
183
+ );
184
+ assert.ok(text.includes('303'), 'env 档必须交代第一次请求拿 303 种下 cookie');
185
+ assert.ok(
186
+ /reuse that jar, not the token/.test(text),
187
+ 'env 档必须点明 token 只用于换 cookie——其余路径带 token 一律 401',
188
+ );
189
+ assert.ok(text.length < 320, `段落已压到 ${String(text.length)} 字符,再涨就该走回那段解释性文案了`);
119
190
  }
120
191
 
121
- // --- 提示词段落 -----------------------------------------------------------
192
+ // --- inline / off 两档 ----------------------------------------------------
122
193
  {
123
- const { captured } = load({ prompt: 'env' });
124
- assert.equal(captured.section.name, 'app:web-auth-url');
125
- assert.equal(captured.section.order, 10110, '段落必须紧跟 WEB_SURFACE(10100)');
126
- const text = captured.section.text();
127
- assert.ok(text.includes('DSH_WEB_AUTH_URL'), 'env 模式必须报出变量名');
128
- assert.ok(!text.includes(TOKEN), 'env 模式绝不能把 token 写进提示词');
129
- assert.ok(text.includes('401') && text.includes('303') && text.includes('$JAR'), 'env 模式必须交代 401 / 303 / cookie jar 用法');
194
+ const { ctx } = await host();
195
+ await mount(ctx, { prompt: 'inline' });
196
+ const assembly = await ctx.systemPrompt.assemble({});
197
+ const text = assembly.sections.find((section) => section.name === SECTION_NAME).text;
198
+ const rendered = renderPrompt(assembly);
199
+ assert.ok(rendered.includes(AUTH_URL), 'inline 档必须把带 token 的地址写进段落');
200
+ assert.ok(!rendered.includes('DSH_WEB_AUTH_URL'), 'inline 档不再报变量名');
201
+ assert.ok(
202
+ text.includes(`"${AUTH_URL}"`),
203
+ 'inline 档的地址也要带引号:两档共用同一份模板,只有地址被替换',
204
+ );
205
+ assert.ok(
206
+ /reuse that jar, not the token/.test(text),
207
+ 'inline 档与 env 档同句式,收尾那句同样要在',
208
+ );
209
+ assert.equal(ctx.shellEnv.collect({}).DSH_WEB_AUTH_URL, AUTH_URL, 'inline 档仍须托管环境变量');
130
210
  }
131
211
  {
132
- const { captured } = load({ prompt: 'inline' });
133
- const text = captured.section.text();
134
- assert.ok(text.includes(AUTH_URL), 'inline 模式必须把带 token 的地址写进段落');
135
- assert.ok(!text.includes('DSH_WEB_AUTH_URL'), 'inline 模式不再报变量名');
212
+ const { ctx } = await host();
213
+ await mount(ctx, { prompt: 'off' });
214
+ const sections = (await ctx.systemPrompt.assemble({})).sections;
215
+ assert.ok(!sections.some((section) => section.name === SECTION_NAME), 'off 档不得注册段落');
216
+ assert.equal(ctx.shellEnv.collect({}).DSH_WEB_AUTH_URL, AUTH_URL, 'off 档仍须托管环境变量');
136
217
  }
218
+
219
+ // --- 无 web 部署:注册照常、值与段落都为空 --------------------------------
137
220
  {
138
- const { captured } = load({ prompt: 'off' });
139
- assert.equal(captured.section, undefined, 'off 模式不得注册段落');
140
- assert.ok(captured.contributor !== undefined, 'off 模式仍须登记环境变量');
221
+ const { ctx } = await host({ web: false });
222
+ await mount(ctx, { prompt: 'env' });
223
+
224
+ assert.equal('DSH_WEB_AUTH_URL' in ctx.shellEnv.collect({}), false, '缺 connection 时不得注入该变量');
225
+
226
+ const assembly = await ctx.systemPrompt.assemble({});
227
+ const mine = assembly.sections.find((section) => section.name === SECTION_NAME);
228
+ assert.notEqual(mine, undefined, '段落注册不依赖 web 部署,照常完成');
229
+ assert.equal(mine.text, '', '缺 connection 时段落必须渲染成空串');
230
+ assert.ok(
231
+ !renderPrompt(assembly).includes('needs auth'),
232
+ '空段落由真实 renderPrompt 过滤,系统提示词里不留痕迹',
233
+ );
141
234
  }
142
235
 
143
- // --- 守卫自身的反例断言(守卫失效即失败) ---------------------------------
236
+ // --- 守卫反例:cordis 真的拒绝未声明服务的属性读取 ------------------------
144
237
  {
145
- const ctx = makeContext(webServices());
146
- assert.throws(() => ctx.shellEnv, /without inject/, '读未声明服务必须抛错,否则上面的断言全是空转');
238
+ const ctx = new Context();
239
+ await ctx.plugin(ShellEnv, { dshHome: join(tmpdir(), 'dsh-web-auth-url-smoke') });
240
+ await assert.rejects(
241
+ async () => {
242
+ await ctx.plugin({ name: 'guarded', apply: (guardedCtx) => void guardedCtx.shellEnv });
243
+ },
244
+ /cannot get property "shellEnv" without inject/,
245
+ '未声明服务必须抛错,否则本插件用 ctx.inject([...]) 取服务就不是必需的了',
246
+ );
147
247
  assert.equal(ctx.get('shellEnv') !== undefined, true, 'ctx.get 不抛错,可选能力走它');
148
248
  }
149
249
 
150
- console.log('smoke-host: ok');
250
+ console.log('smoke-host: ok');