dsh-apis-plugin 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/api-tools.js +69 -23
- package/client.js +50 -7
- package/cordis.patch.yml +2 -1
- package/package.json +1 -1
package/api-tools.js
CHANGED
|
@@ -6,16 +6,29 @@ import { readFileSync, writeFileSync } from 'node:fs'
|
|
|
6
6
|
import { join } from 'node:path'
|
|
7
7
|
import Schema from '@deepseek-ai/schemastery'
|
|
8
8
|
|
|
9
|
-
// 读取目录下
|
|
10
|
-
|
|
11
|
-
|
|
9
|
+
// 读取目录下 api.cfg(key=value 行,# 注释):baseUrl 为服务前缀;apis 为接口列表,
|
|
10
|
+
// 支持逗号分隔同行书写,或 apis= 下面逐行一个接口
|
|
11
|
+
function loadApiCfg(dir) {
|
|
12
|
+
const empty = { apis: [] }
|
|
13
|
+
if (!dir) return empty
|
|
12
14
|
try {
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
15
|
+
const cfg = { apis: [] }
|
|
16
|
+
let current = null
|
|
17
|
+
for (const raw of readFileSync(join(dir, 'api.cfg'), 'utf8').split(/\r?\n/)) {
|
|
18
|
+
const line = raw.trim()
|
|
19
|
+
if (!line || line.startsWith('#')) continue
|
|
20
|
+
const m = line.match(/^([\w-]+)\s*=\s*(.*)$/)
|
|
21
|
+
if (m) {
|
|
22
|
+
current = m[1]
|
|
23
|
+
if (m[2]) cfg[current] = current === 'apis' ? m[2].split(',').map((s) => s.trim()).filter(Boolean) : m[2]
|
|
24
|
+
continue
|
|
25
|
+
}
|
|
26
|
+
// 无 = 的行归属上一个 key(apis 的逐行列举形式)
|
|
27
|
+
if (current === 'apis') cfg.apis.push(line)
|
|
28
|
+
}
|
|
29
|
+
return cfg
|
|
17
30
|
} catch {
|
|
18
|
-
return
|
|
31
|
+
return empty
|
|
19
32
|
}
|
|
20
33
|
}
|
|
21
34
|
|
|
@@ -62,7 +75,7 @@ export function syncClientNs(dir, ns) {
|
|
|
62
75
|
* @param {string} options.ns settings 命名空间,须与前端卡片的 NS 一致
|
|
63
76
|
* @param {string} [options.toolPrefix] 工具名前缀默认值,生成 {prefix}_api_doc / {prefix}_api_request,可被 config 覆盖
|
|
64
77
|
* @param {string} [options.domain] 领域名默认值,用于拼接工具描述 prompt,可被 config 覆盖
|
|
65
|
-
* @param {string} [options.apiDir]
|
|
78
|
+
* @param {string} [options.apiDir] api.cfg 与 api_doc.md 所在目录默认值,可被 config 覆盖
|
|
66
79
|
* @param {string} [options.defaultBaseUrl] 接口服务前缀默认值(域名 + nginx 前缀 + 网关路由段)
|
|
67
80
|
* @param {string[]} [options.defaultEndpoints] yml 之外的额外默认接口
|
|
68
81
|
*/
|
|
@@ -74,12 +87,14 @@ export function defineApiPlugin(options) {
|
|
|
74
87
|
domain: Schema.string().default(domain),
|
|
75
88
|
// 工具名前缀:生成 {prefix}_api_doc / {prefix}_api_request
|
|
76
89
|
toolPrefix: Schema.string().default(toolPrefix),
|
|
77
|
-
//
|
|
90
|
+
// api.cfg 与 api_doc.md 所在目录(留空则只用 endpoints)
|
|
78
91
|
apiDir: Schema.string().default(apiDir),
|
|
79
92
|
endpoints: Schema.array(Schema.string()).default([]),
|
|
80
93
|
// 接口服务前缀:域名 + nginx 前缀 + 网关路由段
|
|
81
94
|
// (接口路径本身已含服务前缀,最终形如 /prod-api/xxx/users/detail)
|
|
82
95
|
baseUrl: Schema.string().default(defaultBaseUrl),
|
|
96
|
+
// 内部字段:客户端「加载API」按钮改写它触发服务端重扫目录
|
|
97
|
+
scanToken: Schema.string().default(''),
|
|
83
98
|
})
|
|
84
99
|
|
|
85
100
|
// 依赖部署挂载的设置服务与工具服务
|
|
@@ -89,24 +104,50 @@ export function defineApiPlugin(options) {
|
|
|
89
104
|
// config 覆盖优先,回退工厂默认值(兼容旧用户层保存值缺字段的情况)
|
|
90
105
|
const prefix = config.toolPrefix || toolPrefix
|
|
91
106
|
const domainName = config.domain || domain
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
const endpoints = [...new Set([...loadApisTxt(docDir), ...defaultEndpoints, ...(config.endpoints ?? [])])]
|
|
107
|
+
// yml 侧固定默认接口(扫描 yml 目录 + 工厂默认 + yml 配置),「加载API」重扫时始终保留这部分
|
|
108
|
+
const ymlApiDir = config.apiDir || apiDir
|
|
109
|
+
const ymlEndpoints = [...new Set([...loadApiCfg(ymlApiDir).apis, ...defaultEndpoints, ...(config.endpoints ?? [])])]
|
|
96
110
|
|
|
97
111
|
// 以组合配置为 base 层注册 settings 命名空间,卸载插件时注册随 fiber 一并清理
|
|
98
|
-
const scope = ctx.settings.register(ns, Config, { base: { ...config, endpoints } })
|
|
112
|
+
const scope = ctx.settings.register(ns, Config, { base: { ...config, endpoints: ymlEndpoints } })
|
|
113
|
+
|
|
114
|
+
// 当前生效目录:用户层保存值优先(重启后仍生效),回退部署侧配置
|
|
115
|
+
let docDir = scope.get()?.apiDir || ymlApiDir
|
|
99
116
|
|
|
100
|
-
//
|
|
117
|
+
// 文档按当前生效目录读取;用户层改写 apiDir 后随 watch 刷新
|
|
118
|
+
let docSections = splitDocSections(loadApiDoc(docDir))
|
|
119
|
+
|
|
120
|
+
// 每次已提交变更后收到通知;「加载API」以 scanToken 变化标记,触发目录重扫
|
|
121
|
+
let lastScanToken = scope.get()?.scanToken ?? ''
|
|
101
122
|
ctx.effect(
|
|
102
|
-
() =>
|
|
123
|
+
() => {
|
|
124
|
+
console.log(`[${ns}] watch registering`)
|
|
125
|
+
return scope.watch((next) => {
|
|
126
|
+
try {
|
|
127
|
+
console.log(`[${ns}] watch fired; apiDir=${next.apiDir}, scanToken=${next.scanToken}`)
|
|
128
|
+
const dir = next.apiDir || apiDir
|
|
129
|
+
// 目录变化或显式重扫时重读文档
|
|
130
|
+
if (dir !== docDir || next.scanToken !== lastScanToken) {
|
|
131
|
+
docDir = dir
|
|
132
|
+
docSections = splitDocSections(loadApiDoc(dir))
|
|
133
|
+
}
|
|
134
|
+
if (next.scanToken !== lastScanToken) {
|
|
135
|
+
lastScanToken = next.scanToken
|
|
136
|
+
// 扫描结果 ∪ yml 默认接口,整体接管用户层 endpoints(去重;值未变时不提交,避免循环)
|
|
137
|
+
const merged = [...new Set([...loadApiCfg(dir).apis, ...ymlEndpoints])]
|
|
138
|
+
const cur = Array.isArray(next.endpoints) ? next.endpoints : []
|
|
139
|
+
if (merged.join('\n') !== cur.join('\n')) scope.update({ endpoints: merged })
|
|
140
|
+
}
|
|
103
141
|
console.log(`${ns}: settings updated`)
|
|
104
|
-
|
|
142
|
+
} catch (err) {
|
|
143
|
+
console.error(`[${ns}] watch handler error:`, err)
|
|
144
|
+
}
|
|
145
|
+
})
|
|
146
|
+
},
|
|
105
147
|
`${ns}: settings watch`,
|
|
106
|
-
|
|
148
|
+
)
|
|
107
149
|
|
|
108
150
|
// ---------- agent 工具 ----------
|
|
109
|
-
const docSections = splitDocSections(loadApiDoc(docDir))
|
|
110
151
|
|
|
111
152
|
// 当前生效的接口列表(用户层保存值优先,回退默认值)
|
|
112
153
|
function currentEndpoints() {
|
|
@@ -114,7 +155,7 @@ export function defineApiPlugin(options) {
|
|
|
114
155
|
const value = scope.get()
|
|
115
156
|
if (Array.isArray(value?.endpoints) && value.endpoints.length) return value.endpoints
|
|
116
157
|
} catch { /* scope.get 不可用时回退 */ }
|
|
117
|
-
return
|
|
158
|
+
return ymlEndpoints
|
|
118
159
|
}
|
|
119
160
|
|
|
120
161
|
// 工具 1:查接口文档,让模型了解每个接口的参数与响应(prompt 按领域名拼接)
|
|
@@ -158,7 +199,12 @@ export function defineApiPlugin(options) {
|
|
|
158
199
|
return `接口 ${norm} 不在已配置列表中。可用接口:\n` + list.join('\n')
|
|
159
200
|
}
|
|
160
201
|
|
|
161
|
-
|
|
202
|
+
// 服务前缀:api.cfg 优先(改文件即生效,无需重启),回退部署配置
|
|
203
|
+
const base = loadApiCfg(docDir).baseUrl || scope.get()?.baseUrl || ''
|
|
204
|
+
if (!base) {
|
|
205
|
+
return `未配置接口服务前缀 baseUrl。请在文档目录 ${docDir || '(未配置)'} 下新建 api.cfg 写入 baseUrl=http://... ,或在部署配置中设置。`
|
|
206
|
+
}
|
|
207
|
+
const url = new URL(base.replace(/\/+$/, '') + matched)
|
|
162
208
|
for (const [k, v] of Object.entries(args.query ?? {})) {
|
|
163
209
|
url.searchParams.set(k, String(v))
|
|
164
210
|
}
|
|
@@ -178,7 +224,7 @@ export function defineApiPlugin(options) {
|
|
|
178
224
|
},
|
|
179
225
|
})
|
|
180
226
|
|
|
181
|
-
console.log(`[${ns}] loaded; endpoints=${
|
|
227
|
+
console.log(`[${ns}] loaded; endpoints=${ymlEndpoints.length}, doc sections=${docSections.length}`)
|
|
182
228
|
}
|
|
183
229
|
|
|
184
230
|
return { Config, inject, apply }
|
package/client.js
CHANGED
|
@@ -34,6 +34,8 @@ window.__ModuleLoader__.load({
|
|
|
34
34
|
.apis-save{background:var(--dsw-alias-label-primary);color:var(--dsw-alias-bg-layer-3)}
|
|
35
35
|
.apis-row{display:flex;align-items:center;gap:8px}
|
|
36
36
|
.apis-rowLabel{width:44px;flex-shrink:0;font-size:13px;color:var(--dsw-alias-label-primary)}
|
|
37
|
+
.apis-rowLabelWide{width:auto;white-space:nowrap}
|
|
38
|
+
.apis-hr{width:100%;height:0;border:0;border-top:.5px solid var(--dsw-alias-border-l2);margin:0}
|
|
37
39
|
.apis-input{box-sizing:border-box;flex:1;min-width:0;font:inherit;color:var(--dsw-alias-label-primary);background:transparent;border:.5px solid var(--dsw-alias-border-l4);border-radius:8px;padding:6px 10px}
|
|
38
40
|
.apis-remove{appearance:none;font:inherit;font-size:13px;cursor:pointer;background:none;border:none;border-radius:8px;padding:5px 10px;color:var(--dsw-alias-label-tertiary)}
|
|
39
41
|
.apis-remove:hover:not(:disabled){color:var(--dsw-alias-label-primary)}
|
|
@@ -80,25 +82,49 @@ window.__ModuleLoader__.load({
|
|
|
80
82
|
function ApisCard(props) {
|
|
81
83
|
const state = props.useApisCard((s) => s);
|
|
82
84
|
const [draft, setDraft] = react.useState(null); // string[],null 表示未编辑
|
|
85
|
+
const [dirDraft, setDirDraft] = react.useState(null); // 文档目录,null 表示未编辑
|
|
83
86
|
const [saving, setSaving] = react.useState(false);
|
|
87
|
+
const [loading, setLoading] = react.useState(false); // 加载API 扫描中
|
|
84
88
|
const [saved, setSaved] = react.useState(false); // 保存成功后短暂提示
|
|
89
|
+
const [loaded, setLoaded] = react.useState(false); // 加载API 完成后短暂提示
|
|
85
90
|
const [open, setOpen] = react.useState(false); // 折叠态只显示标题行
|
|
86
91
|
const ready = state.status === "ready" && state.writable;
|
|
87
92
|
|
|
88
93
|
/** 当前展示的接口列表:编辑中取 draft,否则取已保存值 */
|
|
89
94
|
const list = draft ?? (state.value?.endpoints ?? []).map(String);
|
|
90
95
|
const dirty = draft !== null;
|
|
96
|
+
/** 当前展示的文档目录:编辑中取 dirDraft,否则取已保存值 */
|
|
97
|
+
const savedDir = state.value?.apiDir ?? "";
|
|
98
|
+
const shownDir = dirDraft ?? savedDir;
|
|
99
|
+
const dirDirty = dirDraft !== null && dirDraft !== savedDir;
|
|
91
100
|
|
|
92
101
|
async function save() {
|
|
93
|
-
if (saving || !ready || !dirty) return;
|
|
102
|
+
if (saving || !ready || (!dirty && !dirDirty)) return;
|
|
94
103
|
setSaving(true);
|
|
95
|
-
await scope.set("
|
|
104
|
+
if (dirDirty) await scope.set("apiDir", dirDraft); // 只存配置,扫描交给「加载API」
|
|
105
|
+
if (dirty) await scope.set("endpoints", draft);
|
|
96
106
|
setSaving(false);
|
|
97
107
|
setDraft(null);
|
|
108
|
+
setDirDraft(null);
|
|
98
109
|
setSaved(true); // 显示「已保存」提示,2 秒后消失
|
|
99
110
|
setTimeout(() => setSaved(false), 2000);
|
|
100
111
|
}
|
|
101
112
|
|
|
113
|
+
/** 加载API:把目录写入配置并翻转 scanToken,服务端 watch 重扫 apis.txt 后回写接口列表 */
|
|
114
|
+
async function loadApis() {
|
|
115
|
+
if (loading || !ready || !shownDir.trim()) return;
|
|
116
|
+
setLoading(true);
|
|
117
|
+
try {
|
|
118
|
+
if (dirDirty) await scope.set("apiDir", shownDir.trim());
|
|
119
|
+
await scope.set("scanToken", String(Date.now()));
|
|
120
|
+
setDirDraft(null);
|
|
121
|
+
setLoaded(true);
|
|
122
|
+
setTimeout(() => setLoaded(false), 2000);
|
|
123
|
+
} finally {
|
|
124
|
+
setLoading(false);
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
|
|
102
128
|
// 单行:接口N 标签 + 输入框 + 删除按钮
|
|
103
129
|
const row = (v, i) => react_jsx_runtime.jsxs("div", {
|
|
104
130
|
className: "apis-row",
|
|
@@ -135,8 +161,25 @@ window.__ModuleLoader__.load({
|
|
|
135
161
|
react_jsx_runtime.jsx(Chevron, { open }),
|
|
136
162
|
],
|
|
137
163
|
}),
|
|
138
|
-
//
|
|
164
|
+
// 展开体:API配置目录行 + 分隔线 + 接口行列表 + 底部操作条
|
|
139
165
|
open && react_jsx_runtime.jsxs("div", { className: "apis-body", children: [
|
|
166
|
+
// API配置:文档目录(apis.txt / api_doc.md 所在目录)+ 加载API 重扫按钮
|
|
167
|
+
react_jsx_runtime.jsxs("div", { className: "apis-row", children: [
|
|
168
|
+
react_jsx_runtime.jsx("div", { className: "apis-rowLabel apis-rowLabelWide", children: "API配置" }),
|
|
169
|
+
react_jsx_runtime.jsx("input", {
|
|
170
|
+
value: shownDir, disabled: !ready || saving || loading,
|
|
171
|
+
onChange: (e) => setDirDraft(e.target.value),
|
|
172
|
+
placeholder: "api.cfg / api_doc.md 所在目录",
|
|
173
|
+
className: "apis-input",
|
|
174
|
+
}),
|
|
175
|
+
react_jsx_runtime.jsx("button", {
|
|
176
|
+
type: "button", disabled: !ready || saving || loading || !shownDir.trim(),
|
|
177
|
+
onClick: loadApis,
|
|
178
|
+
className: "apis-btn apis-discard", children: loading ? "扫描中…" : "加载API",
|
|
179
|
+
}),
|
|
180
|
+
loaded && react_jsx_runtime.jsx("span", { className: "apis-saved", children: "已加载" }),
|
|
181
|
+
] }),
|
|
182
|
+
react_jsx_runtime.jsx("hr", { className: "apis-hr" }),
|
|
140
183
|
!state.writable && react_jsx_runtime.jsx("span", { className: "apis-description", children: "只读" }),
|
|
141
184
|
list.map(row),
|
|
142
185
|
react_jsx_runtime.jsx("button", {
|
|
@@ -148,12 +191,12 @@ window.__ModuleLoader__.load({
|
|
|
148
191
|
// 保存成功后的短暂提示(占按钮行左侧)
|
|
149
192
|
react_jsx_runtime.jsx("span", { className: "apis-saved", children: saved ? "已保存" : "" }),
|
|
150
193
|
react_jsx_runtime.jsx("button", {
|
|
151
|
-
type: "button", disabled: !dirty || saving,
|
|
152
|
-
onClick: () => setDraft(null),
|
|
153
|
-
|
|
194
|
+
type: "button", disabled: (!dirty && !dirDirty) || saving,
|
|
195
|
+
onClick: () => { setDraft(null); setDirDraft(null); },
|
|
196
|
+
className: "apis-btn apis-discard", children: "放弃修改",
|
|
154
197
|
}),
|
|
155
198
|
react_jsx_runtime.jsx("button", {
|
|
156
|
-
type: "button", disabled: !ready || !dirty || saving,
|
|
199
|
+
type: "button", disabled: !ready || (!dirty && !dirDirty) || saving,
|
|
157
200
|
onClick: save, children: saving ? "保存中…" : "保存",
|
|
158
201
|
className: "apis-btn apis-save",
|
|
159
202
|
}),
|
package/cordis.patch.yml
CHANGED
|
@@ -7,9 +7,10 @@
|
|
|
7
7
|
domain: ""
|
|
8
8
|
# 工具名前缀:生成 {prefix}_api_doc / {prefix}_api_request
|
|
9
9
|
toolPrefix: api
|
|
10
|
-
#
|
|
10
|
+
# api.cfg 与 api_doc.md 所在目录(可选,留空则只用 endpoints)
|
|
11
11
|
apiDir: ""
|
|
12
12
|
# 默认接口列表
|
|
13
13
|
endpoints: []
|
|
14
14
|
# 接口服务前缀:域名 + nginx 前缀 + 网关路由段
|
|
15
|
+
# (也可在 apiDir 下 api.cfg 写 baseUrl=... 覆盖,改文件即生效无需重启)
|
|
15
16
|
baseUrl: ""
|