dsh-plugin-maoxuan-pet 0.2.2 → 0.2.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/CHANGELOG.md +14 -0
- package/README.en.md +2 -1
- package/README.md +2 -1
- package/cordis.patch.yml +2 -1
- package/docs/RELEASE.md +2 -2
- package/index.js +159 -4
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,19 @@
|
|
|
1
1
|
# CHANGELOG
|
|
2
2
|
|
|
3
|
+
## 0.2.3 — 2026-11-01
|
|
4
|
+
|
|
5
|
+
首次安装修复:模型配置自动从宿主 profile 桥接进 headless 子进程,新装机基本零配置。
|
|
6
|
+
|
|
7
|
+
### 修复
|
|
8
|
+
- **新装机必踩的 `MISSING_CREDENTIAL`**:插件每小时调用 `dsh --profile headless` 生成气泡,而随包 `headless` profile 是一个没有任何模型配置的空壳——官方 DeepSeek、中转线路的用户第一次安装都会生成失败,只能看到「离线摘录 + 报错」。
|
|
9
|
+
- 现在**每次生成前自动把宿主 profile patch 里的 llm 条目**(`agent-default-model`、`dsh-llm-pi-ai`、`dsh-llm-deepseek-api-key`)**原样搬到** `$DSH_HOME/storages/dsh-maoxuan-pet.headless-overlay.yml`,并以 `dsh --profile headless --patch <overlay>` 启动子进程:宿主 GUI 能用什么模型(官方 / wocheng 之类任意中转),气泡子进程就能用什么,官方与中转线路零配置可用。
|
|
10
|
+
- 待机行为:宿主 patch 没有 llm 条目(如机器确实没配过模型)时不写 overlay,`MISSING_CREDENTIAL` 报错会转成一句可操作的中文指引(保留原始错误摘要)写入 `state.lastError`;`dsh` 找不到(ENOENT)同样给出指引,其他报错原样透传。
|
|
11
|
+
- overlay 内容变化才落盘(tmp + rename 原子写);文件可随时手动删除,插件每次生成都会重写。子进程 llm 桥接失败不影响正常降级。
|
|
12
|
+
|
|
13
|
+
### 升级行为
|
|
14
|
+
- `profile` 配置默认仍是 `headless`。显式改 `profile` / `dshBin` 的老配置完全不受影响(此时不做桥接桥外干扰,`--patch` 只在 overlay 存在时追加)。
|
|
15
|
+
- 新增并导出纯函数 `headlessOverlayPath` / `hostPatchPath` / `extractLlmPatchEntries` / `syncHeadlessOverlay` / `friendlyHeadlessError`;`verify/contract.mjs` 增加 llm 抽取与友好报错用例(40 断言全绿)。
|
|
16
|
+
|
|
3
17
|
## 0.2.2 — 2026-11-01
|
|
4
18
|
|
|
5
19
|
展示层维护版:README 从技术说明改成安装页,npm 页面的介绍随之更新。
|
package/README.en.md
CHANGED
|
@@ -55,7 +55,8 @@ fan-made character; not affiliated with DeepSeek.
|
|
|
55
55
|
maxBubbles: 40
|
|
56
56
|
headless: true # false = only local excerpts, zero model calls
|
|
57
57
|
dshBin: dsh
|
|
58
|
-
profile: headless
|
|
58
|
+
profile: headless # since v0.2.3 the host GUI's model config is
|
|
59
|
+
# bridged in automatically (official/relay, no setup)
|
|
59
60
|
timeoutMs: 180000
|
|
60
61
|
businessContext: "40, indie developer, small-tool SaaS"
|
|
61
62
|
```
|
package/README.md
CHANGED
|
@@ -51,7 +51,8 @@ dsh plugin --profile web add dsh-plugin-maoxuan-pet
|
|
|
51
51
|
maxBubbles: 40
|
|
52
52
|
headless: true # false = 只用本地摘录,不花模型调用
|
|
53
53
|
dshBin: dsh
|
|
54
|
-
profile: headless
|
|
54
|
+
profile: headless # v0.2.3 起会自动把宿主 GUI 的模型配置桥接进来
|
|
55
|
+
# (官方/中转线路通用,无需手动配置)
|
|
55
56
|
timeoutMs: 180000
|
|
56
57
|
businessContext: |-
|
|
57
58
|
用户是一位正在创业的独立开发者……
|
package/cordis.patch.yml
CHANGED
|
@@ -16,7 +16,8 @@
|
|
|
16
16
|
quietHours: [2, 8]
|
|
17
17
|
maxBubbles: 40
|
|
18
18
|
headless: true
|
|
19
|
-
profile
|
|
19
|
+
# 不写死 profile:插件默认跟随宿主的 DSH_PROFILE(宿主能用什么模型,
|
|
20
|
+
# 子进程就能用什么模型)。想强制某个 profile 再显式写 profile: xxx。
|
|
20
21
|
timeoutMs: 180000
|
|
21
22
|
businessContext: |-
|
|
22
23
|
用户是一位正在创业的独立开发者/小团队创始人,关心客户获取、现金流、
|
package/docs/RELEASE.md
CHANGED
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
| 项 | 现状(2026-11-01 核对) |
|
|
9
9
|
|---|---|
|
|
10
10
|
| 包名 | `dsh-plugin-maoxuan-pet`(unscoped,npm 上未占用) |
|
|
11
|
-
| 版本 | `0.2.
|
|
11
|
+
| 版本 | `0.2.3` |
|
|
12
12
|
| 代码许可 | MIT(`LICENSE`);语料与立绘的非 MIT 声明见 `NOTICE` |
|
|
13
13
|
| 打包清单 | `npm pack --dry-run` → 59 files / 1.26 MB(含 33 篇语料;0.2.1 起立绘降到 256×539,包体从 4.2 MB 降下来) |
|
|
14
14
|
| GitHub 账号 | `gh auth status` → `yuna78`(scopes 含 `repo`) |
|
|
@@ -93,7 +93,7 @@ cd .. && node build-registry.mjs --write # 重算 registry.json / README 表
|
|
|
93
93
|
node -e "const r=require('./registry.json');console.log(r.plugins.find(p=>p.name==='dsh-plugin-maoxuan-pet'))"
|
|
94
94
|
```
|
|
95
95
|
|
|
96
|
-
期望字段:`openSource: true`、`npm: "0.2.
|
|
96
|
+
期望字段:`openSource: true`、`npm: "0.2.3"`、`market: { title: "yuna78/dsh-maoxuan-pet", version: "0.2.3" }`。
|
|
97
97
|
|
|
98
98
|
## 5. 版本号规则
|
|
99
99
|
|
package/index.js
CHANGED
|
@@ -16,6 +16,7 @@
|
|
|
16
16
|
* node_modules without resolving any DSH-internal module.
|
|
17
17
|
*/
|
|
18
18
|
import { readFile, writeFile, rename, mkdir, stat } from 'node:fs/promises';
|
|
19
|
+
import { readFileSync } from 'node:fs';
|
|
19
20
|
import { spawn } from 'node:child_process';
|
|
20
21
|
import { dirname, join } from 'node:path';
|
|
21
22
|
import { fileURLToPath } from 'node:url';
|
|
@@ -79,6 +80,11 @@ export function normaliseConfig(config) {
|
|
|
79
80
|
dailyLimit: int(raw.dailyLimit, 24, 0, 500),
|
|
80
81
|
headless: raw.headless !== false,
|
|
81
82
|
dshBin: typeof raw.dshBin === 'string' && raw.dshBin.trim() ? raw.dshBin.trim() : 'dsh',
|
|
83
|
+
// The child keeps the dedicated one-shot `headless` profile (its app is the
|
|
84
|
+
// only one that accepts `--json`); the *model* config is bridged from the
|
|
85
|
+
// host profile at spawn time via a `--patch` overlay, so anything the GUI
|
|
86
|
+
// can use — DeepSeek official, a relay, any provider — works with zero
|
|
87
|
+
// per-machine setup. See syncHeadlessOverlay / extractLlmPatchEntries.
|
|
82
88
|
profile: typeof raw.profile === 'string' && raw.profile.trim() ? raw.profile.trim() : 'headless',
|
|
83
89
|
timeoutMs: int(raw.timeoutMs, 180000, 10000, 900000),
|
|
84
90
|
businessContext: typeof raw.businessContext === 'string' ? raw.businessContext.trim() : '',
|
|
@@ -86,6 +92,144 @@ export function normaliseConfig(config) {
|
|
|
86
92
|
};
|
|
87
93
|
}
|
|
88
94
|
|
|
95
|
+
/** Translate a raw child-process failure into something a user can act on. */
|
|
96
|
+
export function friendlyHeadlessError(message) {
|
|
97
|
+
if (/MISSING_CREDENTIAL/i.test(message)) {
|
|
98
|
+
return `${clip(message, 160)}——修复:打开 Web 界面的 Models 页保存一个可用模型的 API Key(插件默认跟随宿主 profile,宿主能用它就能用);或在插件配置里显式指定 profile / dshBin。`;
|
|
99
|
+
}
|
|
100
|
+
if (/无法启动 headless|ENOENT/.test(message)) {
|
|
101
|
+
return `${clip(message, 160)}——多数是 PATH 里没有 dsh 命令,可在插件配置里把 dshBin 指到 dsh 的绝对路径。`;
|
|
102
|
+
}
|
|
103
|
+
return message;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
// --- host-profile → headless child: llm config bridge (v0.2.3) ---------------
|
|
107
|
+
// The hourly child runs `dsh --profile headless`, whose bundled patch carries no
|
|
108
|
+
// llm provider — so on a fresh install it always died with MISSING_CREDENTIAL
|
|
109
|
+
// unless the user happened to export a global DEEPSEEK_API_KEY. The host
|
|
110
|
+
// process *does* have a working model stack (that is what the GUI renders with):
|
|
111
|
+
// its profile patch declares `agent-default-model` and the provider plugins.
|
|
112
|
+
// These two files are plain YAML; bridging is a verbatim copy of the llm
|
|
113
|
+
// entries into a `--patch` overlay handed to the child at spawn time.
|
|
114
|
+
|
|
115
|
+
/** The overlay file handed to every headless child (idempotent, human-inspectable). */
|
|
116
|
+
export function headlessOverlayPath(env = process.env) {
|
|
117
|
+
return join(resolveDshHome(env), 'storages', 'dsh-maoxuan-pet.headless-overlay.yml');
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
/** Where the host profile's own patch layer lives, or null if unknown. */
|
|
121
|
+
export function hostPatchPath(env = process.env) {
|
|
122
|
+
const dir = env.DSH_PROFILE_DIR?.trim();
|
|
123
|
+
if (dir) return join(dir, 'cordis.patch.yml');
|
|
124
|
+
const home = resolveDshHome(env);
|
|
125
|
+
const profile = env.DSH_PROFILE?.trim();
|
|
126
|
+
if (profile) return join(home, 'profiles', profile, 'cordis.patch.yml');
|
|
127
|
+
return null;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
/**
|
|
131
|
+
* Copy the llm-related top-level entries out of a profile patch, verbatim.
|
|
132
|
+
* Only Node builtins are allowed in this package, so "parsing" is slicing the
|
|
133
|
+
* top-level `- ` blocks by column-0 boundary lines; profile patches are
|
|
134
|
+
* machine-written simple YAML, and the output is applied by dsh itself, exactly
|
|
135
|
+
* like the original layer. Anything not matching is left out untouched.
|
|
136
|
+
*/
|
|
137
|
+
export function extractLlmPatchEntries(patchText) {
|
|
138
|
+
const lines = String(patchText ?? '').split('\n');
|
|
139
|
+
const starts = [];
|
|
140
|
+
lines.forEach((line, index) => {
|
|
141
|
+
if (/^- /.test(line)) starts.push(index);
|
|
142
|
+
});
|
|
143
|
+
return starts
|
|
144
|
+
.map((start, i) => lines.slice(start, starts[i + 1] ?? lines.length).join('\n').replace(/\s+$/, ''))
|
|
145
|
+
.filter((block) => /- id: agent-default-model\b/.test(block)
|
|
146
|
+
|| /^\s*name:\s+["']?@deepseek-ai\/dsh-llm-pi-ai["']?\s*$/m.test(block)
|
|
147
|
+
|| /^\s*name:\s+["']?@deepseek-ai\/dsh-llm-deepseek-api-key["']?\s*$/m.test(block));
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
/**
|
|
151
|
+
* Write the overlay if its content changed; return the path, or '' when the
|
|
152
|
+
* host patch had no llm entries (then an existing overlay is kept — a stale
|
|
153
|
+
* bridge still beats a child with no model config at all).
|
|
154
|
+
*/
|
|
155
|
+
export async function syncHeadlessOverlay(env = process.env) {
|
|
156
|
+
const target = headlessOverlayPath(env);
|
|
157
|
+
let text = '';
|
|
158
|
+
const source = hostPatchPath(env);
|
|
159
|
+
if (source) {
|
|
160
|
+
try {
|
|
161
|
+
const entries = extractLlmPatchEntries(await readFile(source, 'utf8'));
|
|
162
|
+
if (entries.length) {
|
|
163
|
+
text = [
|
|
164
|
+
'# Auto-generated by dsh-plugin-maoxuan-pet (v0.2.3).',
|
|
165
|
+
'# The host profile\'s llm entries, bridged verbatim so the hourly',
|
|
166
|
+
'# headless child uses the same model stack as the GUI. Safe to delete:',
|
|
167
|
+
'# the plugin rewrites it on every generation attempt.',
|
|
168
|
+
...entries,
|
|
169
|
+
'',
|
|
170
|
+
].join('\n');
|
|
171
|
+
}
|
|
172
|
+
} catch { /* unreadable host patch → no bridge this run */ }
|
|
173
|
+
}
|
|
174
|
+
let existing = '';
|
|
175
|
+
try { existing = await readFile(target, 'utf8'); } catch { /* absent */ }
|
|
176
|
+
if (existing === text) return text ? target : '';
|
|
177
|
+
await mkdir(dirname(target), { recursive: true });
|
|
178
|
+
const tmp = `${target}.tmp`;
|
|
179
|
+
await writeFile(tmp, text, 'utf8');
|
|
180
|
+
await rename(tmp, target);
|
|
181
|
+
return text ? target : '';
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
/**
|
|
185
|
+
* Read the machine-local credential refs file the web Models page writes
|
|
186
|
+
* (`$DSH_HOME/.credentials.yaml`, only the top-level `refs:` block; the file
|
|
187
|
+
* belongs to the user and is remounted by the harness anyway). Returns a map
|
|
188
|
+
* name → value, {} when absent/unreadable — callers treat any error as
|
|
189
|
+
* "no bridge" and fall back to the honest offline excerpt.
|
|
190
|
+
*/
|
|
191
|
+
export function readCredentialRefs(env = process.env) {
|
|
192
|
+
try {
|
|
193
|
+
const file = join(resolveDshHome(env), '.credentials.yaml');
|
|
194
|
+
const text = readFileSync(file, 'utf8');
|
|
195
|
+
const refs = {};
|
|
196
|
+
let inRefs = false;
|
|
197
|
+
for (const line of text.split('\n')) {
|
|
198
|
+
if (/^refs:/.test(line)) { inRefs = true; continue; }
|
|
199
|
+
if (inRefs && /^[A-Za-z0-9_]+:/.test(line)) break; // next top-level block
|
|
200
|
+
const match = inRefs && /^ ([A-Za-z0-9_]+): (.+)$/.exec(line);
|
|
201
|
+
if (match) refs[match[1]] = match[2].trim();
|
|
202
|
+
}
|
|
203
|
+
return refs;
|
|
204
|
+
} catch {
|
|
205
|
+
return {};
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
/**
|
|
210
|
+
* The child env for one generation: the host env plus every `apiKeyEnv` the
|
|
211
|
+
* bridged overlay names but the child env does not have, sourced from the
|
|
212
|
+
* credential refs (the same store the GUI reads). Injecting only missing names
|
|
213
|
+
* keeps explicit user-provided env values authoritative; anything unknown is
|
|
214
|
+
* simply not injected, so the child fails with the honest MISSING_CREDENTIAL.
|
|
215
|
+
*/
|
|
216
|
+
export function childHeadlessEnv(overlayText, childEnv = process.env) {
|
|
217
|
+
const env = { ...childEnv, DSH_MAOXUAN_PET_CHILD: '1' };
|
|
218
|
+
const names = new Set(
|
|
219
|
+
(String(overlayText ?? '').match(/^\s*apiKeyEnv:\s*([A-Za-z0-9_]+)\s*$/gm) ?? [])
|
|
220
|
+
.map((line) => /^ apiKeyEnv:/.test(line) ? line.trim().replace(/^apiKeyEnv:\s*/, '') : line.replace(/^\s*apiKeyEnv:\s*/, '').trim())
|
|
221
|
+
.map((name) => name.trim()),
|
|
222
|
+
);
|
|
223
|
+
if (!names.size) return env;
|
|
224
|
+
const refs = readCredentialRefs(childEnv);
|
|
225
|
+
for (const name of names) {
|
|
226
|
+
if (env[name]) continue;
|
|
227
|
+
const value = refs[name];
|
|
228
|
+
if (value) env[name] = value;
|
|
229
|
+
}
|
|
230
|
+
return env;
|
|
231
|
+
}
|
|
232
|
+
|
|
89
233
|
/** Resolve DSH home the same way the harness does. */
|
|
90
234
|
export function resolveDshHome(env = process.env) {
|
|
91
235
|
if (env.DSH_HOME && env.DSH_HOME.trim()) return env.DSH_HOME.trim();
|
|
@@ -668,13 +812,24 @@ export function apply(ctx, config) {
|
|
|
668
812
|
};
|
|
669
813
|
|
|
670
814
|
/** Run one headless dsh conversation and return the final text, or throw. */
|
|
671
|
-
const runHeadless = (prompt) => new Promise((resolvePromise, reject) => {
|
|
672
|
-
|
|
815
|
+
const runHeadless = (prompt) => new Promise(async (resolvePromise, reject) => {
|
|
816
|
+
// Bridge the host profile's llm config into the one-shot child (idempotent;
|
|
817
|
+
// '' when nothing to bridge — see syncHeadlessOverlay). Must not kill the
|
|
818
|
+
// generation if it fails for any reason: worst case the child errors with
|
|
819
|
+
// the old MISSING_CREDENTIAL and the usual offline fallback kicks in.
|
|
820
|
+
await syncHeadlessOverlay().catch(() => '');
|
|
821
|
+
let overlayText = '';
|
|
822
|
+
try {
|
|
823
|
+
overlayText = await readFile(headlessOverlayPath(), 'utf8');
|
|
824
|
+
} catch { /* no overlay available this run */ }
|
|
825
|
+
const args = ['--profile', cfg.profile];
|
|
826
|
+
if (overlayText.trim()) args.push('--patch', headlessOverlayPath());
|
|
827
|
+
args.push('--json', prompt);
|
|
673
828
|
let proc;
|
|
674
829
|
try {
|
|
675
830
|
proc = spawn(cfg.dshBin, args, {
|
|
676
831
|
cwd: homedir(),
|
|
677
|
-
env:
|
|
832
|
+
env: childHeadlessEnv(overlayText),
|
|
678
833
|
stdio: ['ignore', 'pipe', 'pipe'],
|
|
679
834
|
});
|
|
680
835
|
} catch (error) {
|
|
@@ -810,7 +965,7 @@ export function apply(ctx, config) {
|
|
|
810
965
|
if (generated) produced = { mode: 'headless', ...generated };
|
|
811
966
|
else failure = `模型输出无法解析为约定 JSON(${clip(answer, 120)})`;
|
|
812
967
|
} catch (error) {
|
|
813
|
-
failure = String(error?.message ?? error);
|
|
968
|
+
failure = friendlyHeadlessError(String(error?.message ?? error));
|
|
814
969
|
}
|
|
815
970
|
}
|
|
816
971
|
|