dsh-enterprise-knowledge-steward 0.0.0-stage → 1.0.0

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/.env.example ADDED
@@ -0,0 +1,5 @@
1
+ # 复制为本目录下的 .env 后填写(不要提交真密码)
2
+ KMS_BASE=http://cgzhdn.shudaodsj.com:50004
3
+ KMS_USERNAME=
4
+ KMS_PASSWORD=
5
+ ZHDN_BASE=http://cgzhdn.shudaodsj.com:50005
package/README.md CHANGED
@@ -1,3 +1,57 @@
1
- # Temporary Holding Version
1
+ # 企业知识管家(DeepSeek Harness 插件)
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
3
+ 制度与知识问答、清分数据、人员任职与信访台账。
4
+
5
+ 包名:`dsh-enterprise-knowledge-steward`
6
+
7
+ ## 安装(npm 包名拉取)
8
+
9
+ 桌面版:设置 → 插件 → 安装框填入包名:
10
+
11
+ ```
12
+ dsh-enterprise-knowledge-steward
13
+ ```
14
+
15
+ CLI:
16
+
17
+ ```bash
18
+ # 桌面版 profile
19
+ dsh plugin --profile desktop add dsh-enterprise-knowledge-steward
20
+
21
+ # Web profile
22
+ dsh plugin --profile web add dsh-enterprise-knowledge-steward
23
+
24
+ # 终端 TUI(按本机实际 profile 名)
25
+ dsh plugin --profile dsh-tui add dsh-enterprise-knowledge-steward
26
+ ```
27
+
28
+ 无全局 `dsh` 时可用:
29
+
30
+ ```bash
31
+ npx @deepseek-ai/dsh plugin --profile desktop add dsh-enterprise-knowledge-steward
32
+ ```
33
+
34
+ 安装后**完全退出再打开** DSH。
35
+
36
+ ## 配置账密
37
+
38
+ 在插件安装目录复制 `.env.example` 为 `.env`,填写:
39
+
40
+ ```
41
+ KMS_USERNAME=你的账号
42
+ KMS_PASSWORD=你的密码
43
+ ```
44
+
45
+ 只需一套 50004(KMS)账密;`KMS_BASE` / `ZHDN_BASE` 一般不用改。**不要把 `.env` 提交或发布到 npm。**
46
+
47
+ ## 使用
48
+
49
+ 打开对话直接提问,例如「查一下人员任职台账」「川高公司收费清分管理办法」。
50
+ 也可点名工具:`ledger_personnel`、`zhdn_ask`、`knowledge_list` 等。
51
+
52
+ ## 本地目录 / tgz(可选)
53
+
54
+ ```bash
55
+ dsh plugin --profile desktop add ./企业知识管家-dsh-plugin
56
+ dsh plugin --profile desktop add ./dsh-enterprise-knowledge-steward-1.0.0.tgz
57
+ ```
@@ -0,0 +1,3 @@
1
+ - insert:
2
+ - id: enterprise-knowledge-steward
3
+ name: dsh-enterprise-knowledge-steward
package/index.js ADDED
@@ -0,0 +1,253 @@
1
+ import { defineTool } from '@deepseek-ai/dsh-tools'
2
+ import { loadDotEnv } from './lib/env.js'
3
+ import {
4
+ doLogin,
5
+ clearLogin,
6
+ ledgerPersonnel,
7
+ ledgerPetition,
8
+ ledgerYangguan,
9
+ ledgerJianshe,
10
+ zhdnAsk,
11
+ knowledgeList,
12
+ } from './lib/api.js'
13
+
14
+ export const name = 'enterprise-knowledge-steward'
15
+ export const inject = ['tools']
16
+
17
+ const textOut = {
18
+ schema: {
19
+ type: 'object',
20
+ additionalProperties: false,
21
+ properties: {
22
+ ok: { type: 'boolean', required: true },
23
+ summary: { type: 'string', required: true },
24
+ data: { type: 'object', additionalProperties: true },
25
+ },
26
+ },
27
+ render(_args, value) {
28
+ return [{ type: 'text', text: value.summary }]
29
+ },
30
+ }
31
+
32
+ function summarizeJson(obj, max = 3500) {
33
+ const s = typeof obj === 'string' ? obj : JSON.stringify(obj, null, 2)
34
+ if (s.length <= max) return s
35
+ return s.slice(0, max) + '\n…(已截断)'
36
+ }
37
+
38
+ function okResult(summary, data) {
39
+ return { ok: true, summary, data: data || {} }
40
+ }
41
+
42
+ function errResult(e) {
43
+ return {
44
+ ok: false,
45
+ summary: `失败:${e && e.message ? e.message : String(e)}`,
46
+ data: { code: (e && e.code) || 'ERROR' },
47
+ }
48
+ }
49
+
50
+ export function apply(ctx) {
51
+ loadDotEnv()
52
+
53
+ ctx.tools.register(
54
+ defineTool({
55
+ name: 'kms_login',
56
+ description: '登录企业知识管家(自动缓存;台账与问答前可先调用;force=true 强制重新登录)',
57
+ parameters: {
58
+ force: { type: 'boolean', description: '是否强制重新登录' },
59
+ clear: { type: 'boolean', description: '为 true 时仅清除本地登录缓存' },
60
+ },
61
+ output: textOut,
62
+ async execute(args) {
63
+ try {
64
+ if (args.clear) {
65
+ const r = await clearLogin()
66
+ return okResult('已清除登录缓存', r)
67
+ }
68
+ const r = await doLogin(!!args.force)
69
+ return okResult(`登录成功,用户 ${r.user}`, r)
70
+ } catch (e) {
71
+ return errResult(e)
72
+ }
73
+ },
74
+ presentCall: () => ({ card: 'generic', title: '登录企业知识管家', kind: 'other' }),
75
+ }),
76
+ )
77
+
78
+ ctx.tools.register(
79
+ defineTool({
80
+ name: 'ledger_personnel',
81
+ description: '查询人员任职台账(分页,可选关键词)',
82
+ parameters: {
83
+ page: { type: 'integer', description: '页码,默认 1' },
84
+ page_size: { type: 'integer', description: '每页条数,默认 10' },
85
+ keyword: { type: 'string', description: '关键词' },
86
+ },
87
+ output: textOut,
88
+ async execute(args) {
89
+ try {
90
+ const body = await ledgerPersonnel({
91
+ page: args.page || 1,
92
+ page_size: args.page_size || 10,
93
+ keyword: args.keyword || '',
94
+ })
95
+ const total = body?.data?.total ?? body?.total ?? ''
96
+ return okResult(`人员任职台账查询完成${total !== '' ? `(total=${total})` : ''}\n` + summarizeJson(body), body)
97
+ } catch (e) {
98
+ return errResult(e)
99
+ }
100
+ },
101
+ presentCall: (args) => ({
102
+ card: 'generic',
103
+ title: '人员任职台账',
104
+ kind: 'other',
105
+ rawInput: args,
106
+ }),
107
+ }),
108
+ )
109
+
110
+ ctx.tools.register(
111
+ defineTool({
112
+ name: 'ledger_petition',
113
+ description: '查询信访台账(分页,可选关键词)',
114
+ parameters: {
115
+ page: { type: 'integer', description: '页码,默认 1' },
116
+ per_page: { type: 'integer', description: '每页条数,默认 10' },
117
+ keyword: { type: 'string', description: '关键词' },
118
+ },
119
+ output: textOut,
120
+ async execute(args) {
121
+ try {
122
+ const body = await ledgerPetition({
123
+ page: args.page || 1,
124
+ per_page: args.per_page || 10,
125
+ keyword: args.keyword || '',
126
+ })
127
+ const total = body?.data?.total ?? body?.total ?? ''
128
+ return okResult(`信访台账查询完成${total !== '' ? `(total=${total})` : ''}\n` + summarizeJson(body), body)
129
+ } catch (e) {
130
+ return errResult(e)
131
+ }
132
+ },
133
+ presentCall: (args) => ({
134
+ card: 'generic',
135
+ title: '信访台账',
136
+ kind: 'other',
137
+ rawInput: args,
138
+ }),
139
+ }),
140
+ )
141
+
142
+ ctx.tools.register(
143
+ defineTool({
144
+ name: 'ledger_yangguan',
145
+ description: '养管相关查询',
146
+ parameters: {
147
+ question: { type: 'string', required: true, description: '养管问题' },
148
+ },
149
+ output: textOut,
150
+ async execute(args) {
151
+ try {
152
+ const body = await ledgerYangguan(args.question)
153
+ return okResult('养管查询完成\n' + summarizeJson(body), body)
154
+ } catch (e) {
155
+ return errResult(e)
156
+ }
157
+ },
158
+ presentCall: () => ({ card: 'generic', title: '养管查询', kind: 'other' }),
159
+ }),
160
+ )
161
+
162
+ ctx.tools.register(
163
+ defineTool({
164
+ name: 'ledger_jianshe',
165
+ description: '建设智达相关分析查询',
166
+ parameters: {
167
+ question: { type: 'string', required: true, description: '建设问题' },
168
+ },
169
+ output: textOut,
170
+ async execute(args) {
171
+ try {
172
+ const body = await ledgerJianshe(args.question)
173
+ return okResult('建设查询完成\n' + summarizeJson(body), body)
174
+ } catch (e) {
175
+ return errResult(e)
176
+ }
177
+ },
178
+ presentCall: () => ({ card: 'generic', title: '建设查询', kind: 'other' }),
179
+ }),
180
+ )
181
+
182
+ ctx.tools.register(
183
+ defineTool({
184
+ name: 'zhdn_ask',
185
+ description:
186
+ '智能问答:制度/知识库、清分数据、服务区、养管月报等。按问题内容自动分流。若返回「网络原因暂时无法服务」属后端下游问题,须原文转述用户。',
187
+ parameters: {
188
+ question: { type: 'string', required: true, description: '用户问题' },
189
+ hint: {
190
+ type: 'string',
191
+ description: '可选提示:knowledge_qa | sqlbot | service_area | yhpt',
192
+ },
193
+ session_id: { type: 'string', description: '会话 id(可选)' },
194
+ },
195
+ output: textOut,
196
+ async execute(args) {
197
+ try {
198
+ const r = await zhdnAsk({
199
+ question: args.question,
200
+ hint: args.hint || '',
201
+ session_id: args.session_id || '',
202
+ })
203
+ const refs = Array.isArray(r.reference_docs) ? r.reference_docs.length : 0
204
+ const head = r.type ? `[${r.type}] ` : ''
205
+ const tail = refs ? `\n(参考资料 ${refs} 篇)` : ''
206
+ return okResult(head + (r.answer || '(空回答)') + tail, r)
207
+ } catch (e) {
208
+ return errResult(e)
209
+ }
210
+ },
211
+ presentCall: (args) => ({
212
+ card: 'generic',
213
+ title: '智能问答',
214
+ kind: 'other',
215
+ rawInput: { question: args.question },
216
+ }),
217
+ }),
218
+ )
219
+
220
+ ctx.tools.register(
221
+ defineTool({
222
+ name: 'knowledge_list',
223
+ description: '检索知识库条目列表(文档标题/摘要),用于确认知识库是否有相关制度文档',
224
+ parameters: {
225
+ query: { type: 'string', description: '检索关键词' },
226
+ page: { type: 'integer', description: '页码' },
227
+ per_page: { type: 'integer', description: '每页条数' },
228
+ },
229
+ output: textOut,
230
+ async execute(args) {
231
+ try {
232
+ const r = await knowledgeList({
233
+ query: args.query || '',
234
+ page: args.page || 1,
235
+ per_page: args.per_page || 10,
236
+ })
237
+ const lines = (r.items || []).slice(0, 20).map((it, i) => `${i + 1}. ${it.name} (kb=${it.kb_id}, id=${it.id})`)
238
+ const summary =
239
+ `知识条目 ${r.total} 条` + (lines.length ? `\n` + lines.join('\n') : '\n(无命中)')
240
+ return okResult(summary, r)
241
+ } catch (e) {
242
+ return errResult(e)
243
+ }
244
+ },
245
+ presentCall: (args) => ({
246
+ card: 'generic',
247
+ title: '知识库条目',
248
+ kind: 'other',
249
+ rawInput: args,
250
+ }),
251
+ }),
252
+ )
253
+ }
package/lib/api.js ADDED
@@ -0,0 +1,192 @@
1
+ import { baseUrl, kmsBaseUrl, kmsCredentials, clearAllTokens } from './env.js'
2
+ import { kmsLoginFull, login, withAuth, withKmsAuth } from './auth.js'
3
+
4
+ export async function doLogin(force = false) {
5
+ const full = await kmsLoginFull(!!force)
6
+ const qa = await login(!!force)
7
+ const { username } = kmsCredentials()
8
+ return {
9
+ ok: true,
10
+ user: full.username || username || '',
11
+ kms_base: kmsBaseUrl(),
12
+ zhdn_base: baseUrl(),
13
+ kms_access_len: full.accessToken.length,
14
+ qa_token_len: qa.length,
15
+ }
16
+ }
17
+
18
+ export async function clearLogin() {
19
+ clearAllTokens()
20
+ return { ok: true, cleared: true }
21
+ }
22
+
23
+ async function kmsGet(pathOrUrl) {
24
+ const base = kmsBaseUrl()
25
+ const url = pathOrUrl.startsWith('http')
26
+ ? pathOrUrl
27
+ : base + (pathOrUrl.startsWith('/') ? pathOrUrl : '/' + pathOrUrl)
28
+ const res = await withKmsAuth((headers) =>
29
+ fetch(url, { method: 'GET', headers: { ...headers, Accept: 'application/json' } }),
30
+ )
31
+ const text = await res.text()
32
+ let body
33
+ try {
34
+ body = JSON.parse(text)
35
+ } catch {
36
+ const err = new Error(`台账返回非 JSON HTTP ${res.status}`)
37
+ err.code = 'KMS_BAD_JSON'
38
+ throw err
39
+ }
40
+ return body
41
+ }
42
+
43
+ async function kmsPost(pathOrUrl, payload) {
44
+ const base = kmsBaseUrl()
45
+ const url = pathOrUrl.startsWith('http')
46
+ ? pathOrUrl
47
+ : base + (pathOrUrl.startsWith('/') ? pathOrUrl : '/' + pathOrUrl)
48
+ const res = await withKmsAuth((headers) =>
49
+ fetch(url, {
50
+ method: 'POST',
51
+ headers: {
52
+ ...headers,
53
+ 'Content-Type': 'application/json',
54
+ Accept: 'application/json',
55
+ },
56
+ body: JSON.stringify(payload || {}),
57
+ }),
58
+ )
59
+ const text = await res.text()
60
+ try {
61
+ return JSON.parse(text)
62
+ } catch {
63
+ const err = new Error(`台账返回非 JSON HTTP ${res.status}`)
64
+ err.code = 'KMS_BAD_JSON'
65
+ throw err
66
+ }
67
+ }
68
+
69
+ export async function ledgerPersonnel({ page = 1, page_size = 10, keyword = '' } = {}) {
70
+ const qs = new URLSearchParams({ page: String(page), page_size: String(page_size) })
71
+ if (keyword) qs.set('keyword', keyword)
72
+ return kmsGet(`/api/rytz/personnel?${qs.toString()}`)
73
+ }
74
+
75
+ export async function ledgerPetition({ page = 1, per_page = 10, keyword = '' } = {}) {
76
+ const qs = new URLSearchParams({ page: String(page), per_page: String(per_page) })
77
+ if (keyword) qs.set('keyword', keyword)
78
+ return kmsGet(`/api/project/xftz?${qs.toString()}`)
79
+ }
80
+
81
+ export async function ledgerYangguan(question) {
82
+ return kmsPost('/api/project/yh/query', { question })
83
+ }
84
+
85
+ export async function ledgerJianshe(question) {
86
+ return kmsPost('/api/project/jgpt/analysis', { question })
87
+ }
88
+
89
+ export async function zhdnAsk({ question, hint = '', session_id = '' } = {}) {
90
+ if (!question || !String(question).trim()) {
91
+ const err = new Error('question 不能为空')
92
+ err.code = 'BAD_ARGS'
93
+ throw err
94
+ }
95
+ const form = new FormData()
96
+ form.append('question', String(question).trim())
97
+ if (session_id) form.append('session_id', session_id)
98
+
99
+ const url = `${baseUrl()}/api/agentic_router/chat`
100
+ const res = await withAuth((headers) => fetch(url, { method: 'POST', headers, body: form }))
101
+ const body = await res.json().catch(() => ({}))
102
+ if (!res.ok || body.code !== 0) {
103
+ const err = new Error(
104
+ `问答失败 HTTP ${res.status} code=${body.code} msg=${body.message || ''}`,
105
+ )
106
+ err.code = 'AGENTIC_FAILED'
107
+ throw err
108
+ }
109
+ const data = body.data || {}
110
+ const result = data.result || {}
111
+ const answer =
112
+ result.answer ||
113
+ result.content ||
114
+ (typeof result === 'string' ? result : '') ||
115
+ body.message ||
116
+ ''
117
+ return {
118
+ type: data.type || '',
119
+ hint: hint || undefined,
120
+ answer,
121
+ session_id: result.session_id || '',
122
+ reference_docs: result.reference_docs || [],
123
+ message: body.message || '',
124
+ }
125
+ }
126
+
127
+ async function kmsReq(method, p, body) {
128
+ const url = kmsBaseUrl() + p
129
+ const res = await withKmsAuth((h) =>
130
+ fetch(url, {
131
+ method,
132
+ headers: {
133
+ ...h,
134
+ Accept: 'application/json',
135
+ ...(body ? { 'Content-Type': 'application/json' } : {}),
136
+ },
137
+ body: body ? JSON.stringify(body) : undefined,
138
+ }),
139
+ )
140
+ const text = await res.text()
141
+ try {
142
+ return JSON.parse(text)
143
+ } catch {
144
+ return { code: res.status, message: text.slice(0, 200) }
145
+ }
146
+ }
147
+
148
+ async function listAccessibleKbIds() {
149
+ const ids = new Set()
150
+ const mgmt = await kmsReq('GET', '/api/v1/knowledge_base/management?page=1&per_page=100')
151
+ if (mgmt && mgmt.data) {
152
+ if (Array.isArray(mgmt.data.items)) {
153
+ for (const it of mgmt.data.items) if (it && it.id != null) ids.add(Number(it.id))
154
+ } else if (mgmt.data.id != null) {
155
+ ids.add(Number(mgmt.data.id))
156
+ }
157
+ }
158
+ const list = await kmsReq('GET', '/api/v1/knowledge_base/list?page=1&per_page=100')
159
+ if (list && list.data && Array.isArray(list.data.items)) {
160
+ for (const it of list.data.items) if (it && it.id != null) ids.add(Number(it.id))
161
+ }
162
+ if (ids.size === 0) ids.add(1)
163
+ return [...ids]
164
+ }
165
+
166
+ export async function knowledgeList({ query = '', page = 1, per_page = 10 } = {}) {
167
+ const kbIds = await listAccessibleKbIds()
168
+ const hits = []
169
+ for (const kbId of kbIds) {
170
+ const body = await kmsReq('POST', '/api/v1/knowledge_item/knowledge_list', {
171
+ kb_id: kbId,
172
+ query_cate: 1,
173
+ query: query || '',
174
+ page: Number(page) || 1,
175
+ per_page: Number(per_page) || 10,
176
+ sort: 'update_time',
177
+ revert: 'descend',
178
+ })
179
+ const items = (body && body.data && body.data.items) || []
180
+ for (const it of items) {
181
+ hits.push({
182
+ kb_id: kbId,
183
+ id: it.id,
184
+ name: it.name || it.title || '',
185
+ abstract: it.abstract || '',
186
+ update_time: it.update_time || '',
187
+ department: it.department || '',
188
+ })
189
+ }
190
+ }
191
+ return { total: hits.length, items: hits }
192
+ }
package/lib/auth.js ADDED
@@ -0,0 +1,183 @@
1
+ import crypto from 'node:crypto'
2
+ import { createRequire } from 'node:module'
3
+ import path from 'node:path'
4
+ import { fileURLToPath } from 'node:url'
5
+ import {
6
+ PLUGIN_ROOT,
7
+ TOKEN_KMS_PATH,
8
+ TOKEN_QA_PATH,
9
+ baseUrl,
10
+ kmsBaseUrl,
11
+ kmsCredentials,
12
+ readTokenFile,
13
+ tokenStillValid,
14
+ readCachedToken,
15
+ writeCachedToken,
16
+ clearCachedToken,
17
+ ensureEnv,
18
+ } from './env.js'
19
+
20
+ const require = createRequire(import.meta.url)
21
+ const __dirname = path.dirname(fileURLToPath(import.meta.url))
22
+
23
+ function loadSm2() {
24
+ try {
25
+ return require('sm-crypto').sm2
26
+ } catch {
27
+ try {
28
+ return require(path.join(PLUGIN_ROOT, 'node_modules', 'sm-crypto')).sm2
29
+ } catch {
30
+ const err = new Error('缺少 sm-crypto:请在插件目录执行 npm install')
31
+ err.code = 'NO_SM_CRYPTO'
32
+ throw err
33
+ }
34
+ }
35
+ }
36
+
37
+ /** Full KMS login → { accessToken, ucToken, username } */
38
+ export async function kmsLoginFull(force = false) {
39
+ ensureEnv()
40
+ if (!force) {
41
+ const raw = readTokenFile(TOKEN_KMS_PATH)
42
+ if (raw && raw.token && tokenStillValid(raw.token) && raw.user_center_access_token) {
43
+ return {
44
+ accessToken: raw.token,
45
+ ucToken: raw.user_center_access_token,
46
+ username: raw.username || '',
47
+ }
48
+ }
49
+ }
50
+
51
+ const { username, password } = kmsCredentials()
52
+ if (!username || !password) {
53
+ const err = new Error('未配置账号:请在插件目录 .env 填写 KMS_USERNAME / KMS_PASSWORD')
54
+ err.code = 'NO_KMS_CREDENTIALS'
55
+ throw err
56
+ }
57
+
58
+ const pkRes = await fetch(`${kmsBaseUrl()}/api/search_pangu/get_public_key`, {
59
+ headers: { Accept: 'application/json' },
60
+ })
61
+ const pkBody = await pkRes.json().catch(() => ({}))
62
+ let pub = pkBody && pkBody.data
63
+ if (!pub) {
64
+ const err = new Error(`获取公钥失败 HTTP ${pkRes.status}`)
65
+ err.code = 'KMS_LOGIN_FAILED'
66
+ throw err
67
+ }
68
+ if (!String(pub).startsWith('04')) pub = '04' + pub
69
+
70
+ const digest = crypto.createHash('md5').update(String(password), 'utf8').digest('hex')
71
+ const sm2 = loadSm2()
72
+ const enc = sm2.doEncrypt(digest, pub, 1)
73
+
74
+ const loginRes = await fetch(`${kmsBaseUrl()}/api/search_pangu/login`, {
75
+ method: 'POST',
76
+ headers: { 'Content-Type': 'application/json', Accept: 'application/json' },
77
+ body: JSON.stringify({ username, password: enc }),
78
+ })
79
+ const loginBody = await loginRes.json().catch(() => ({}))
80
+ const data = (loginBody && loginBody.data) || {}
81
+ const accessToken = data.access_token || data.token
82
+ const ucToken = data.user_center_access_token || data.userCenterAccessToken
83
+ if (!accessToken) {
84
+ const err = new Error(
85
+ `登录失败 HTTP ${loginRes.status} code=${loginBody.code} msg=${loginBody.message || ''}`,
86
+ )
87
+ err.code = 'KMS_LOGIN_FAILED'
88
+ throw err
89
+ }
90
+ if (!ucToken) {
91
+ const err = new Error('登录成功但缺少 user_center_access_token')
92
+ err.code = 'KMS_LOGIN_FAILED'
93
+ throw err
94
+ }
95
+
96
+ writeCachedToken(TOKEN_KMS_PATH, accessToken, {
97
+ username,
98
+ user_center_access_token: ucToken,
99
+ })
100
+ return { accessToken, ucToken, username }
101
+ }
102
+
103
+ export async function kmsLogin(force = false) {
104
+ const full = await kmsLoginFull(!!force)
105
+ return full.accessToken
106
+ }
107
+
108
+ export async function exchangeQaToken(ucToken) {
109
+ const url = `${baseUrl()}/api/admin/user/self`
110
+ const res = await fetch(url, {
111
+ method: 'GET',
112
+ headers: { Authorization: ucToken, Accept: 'application/json' },
113
+ })
114
+ const body = await res.json().catch(() => ({}))
115
+ if (!res.ok || body.code !== 0) {
116
+ const err = new Error(
117
+ `会话交换失败 HTTP ${res.status} code=${body.code} msg=${body.message || ''}`,
118
+ )
119
+ err.code = 'QA_EXCHANGE_FAILED'
120
+ throw err
121
+ }
122
+ const t = body.data && body.data.token
123
+ if (typeof t === 'string' && t.length > 20) {
124
+ return { qaToken: t, source: 'self.data.token', user: body.data }
125
+ }
126
+ return { qaToken: ucToken, source: 'user_center_access_token', user: body.data }
127
+ }
128
+
129
+ export async function login(force = false) {
130
+ if (!force) {
131
+ const cached = readCachedToken(TOKEN_QA_PATH)
132
+ if (cached) return cached
133
+ }
134
+ const { ucToken, username } = await kmsLoginFull(!!force)
135
+ const { qaToken, source } = await exchangeQaToken(ucToken)
136
+ writeCachedToken(TOKEN_QA_PATH, qaToken, { username, source })
137
+ return qaToken
138
+ }
139
+
140
+ export async function authHeader(forceLogin = false) {
141
+ const token = await login(!!forceLogin)
142
+ return { Authorization: token }
143
+ }
144
+
145
+ export async function kmsAuthHeader(forceLogin = false) {
146
+ const token = await kmsLogin(!!forceLogin)
147
+ return { Authorization: 'Bearer ' + token }
148
+ }
149
+
150
+ export async function withAuth(doRequest) {
151
+ let headers = await authHeader(false)
152
+ let res = await doRequest(headers)
153
+ if (res.status === 401) {
154
+ clearCachedToken(TOKEN_QA_PATH)
155
+ clearCachedToken(TOKEN_KMS_PATH)
156
+ headers = await authHeader(true)
157
+ res = await doRequest(headers)
158
+ }
159
+ return res
160
+ }
161
+
162
+ export async function withKmsAuth(doRequest) {
163
+ let headers = await kmsAuthHeader(false)
164
+ let res = await doRequest(headers)
165
+ try {
166
+ const bodyText = await res.clone().text()
167
+ const j = JSON.parse(bodyText)
168
+ if (j && (j.code === 401 || j.status === 401)) {
169
+ clearCachedToken(TOKEN_KMS_PATH)
170
+ headers = await kmsAuthHeader(true)
171
+ res = await doRequest(headers)
172
+ return res
173
+ }
174
+ } catch {
175
+ /* ignore */
176
+ }
177
+ if (res.status === 401) {
178
+ clearCachedToken(TOKEN_KMS_PATH)
179
+ headers = await kmsAuthHeader(true)
180
+ res = await doRequest(headers)
181
+ }
182
+ return res
183
+ }
package/lib/env.js ADDED
@@ -0,0 +1,121 @@
1
+ import fs from 'node:fs'
2
+ import path from 'node:path'
3
+ import { fileURLToPath } from 'node:url'
4
+
5
+ const __dirname = path.dirname(fileURLToPath(import.meta.url))
6
+ export const PLUGIN_ROOT = path.resolve(__dirname, '..')
7
+ export const ENV_PATH = path.join(PLUGIN_ROOT, '.env')
8
+ export const TOKEN_QA_PATH = path.join(PLUGIN_ROOT, '.token-qa')
9
+ export const TOKEN_KMS_PATH = path.join(PLUGIN_ROOT, '.token-kms')
10
+
11
+ export const DEFAULT_BASE = 'http://cgzhdn.shudaodsj.com:50005'
12
+ export const DEFAULT_KMS_BASE = 'http://cgzhdn.shudaodsj.com:50004'
13
+
14
+ let loaded = false
15
+
16
+ export function loadDotEnv(extraPath) {
17
+ const candidates = [extraPath, ENV_PATH, path.join(process.cwd(), '.env')].filter(Boolean)
18
+ for (const p of candidates) {
19
+ if (!p || !fs.existsSync(p)) continue
20
+ const text = fs.readFileSync(p, 'utf8')
21
+ for (const line of text.split(/\r?\n/)) {
22
+ const s = line.trim()
23
+ if (!s || s.startsWith('#')) continue
24
+ const i = s.indexOf('=')
25
+ if (i < 0) continue
26
+ const k = s.slice(0, i).trim()
27
+ let v = s.slice(i + 1).trim()
28
+ if ((v.startsWith('"') && v.endsWith('"')) || (v.startsWith("'") && v.endsWith("'"))) {
29
+ v = v.slice(1, -1)
30
+ }
31
+ if (!(k in process.env) || process.env[k] === '') process.env[k] = v
32
+ }
33
+ }
34
+ loaded = true
35
+ }
36
+
37
+ export function ensureEnv() {
38
+ if (!loaded) loadDotEnv()
39
+ }
40
+
41
+ export function baseUrl() {
42
+ ensureEnv()
43
+ return (process.env.ZHDN_BASE || DEFAULT_BASE).replace(/\/$/, '')
44
+ }
45
+
46
+ export function kmsBaseUrl() {
47
+ ensureEnv()
48
+ return (
49
+ process.env.KMS_BASE ||
50
+ process.env.ZHDN_KMS_BASE ||
51
+ process.env.KMS_BASE_URL ||
52
+ DEFAULT_KMS_BASE
53
+ ).replace(/\/$/, '')
54
+ }
55
+
56
+ export function kmsCredentials() {
57
+ ensureEnv()
58
+ const username =
59
+ process.env.KMS_USERNAME ||
60
+ process.env.ZHDN_KMS_USERNAME ||
61
+ process.env.ZHDN_USERNAME ||
62
+ ''
63
+ const password = process.env.KMS_PASSWORD || process.env.ZHDN_KMS_PASSWORD || ''
64
+ return { username, password }
65
+ }
66
+
67
+ export function readTokenFile(filePath) {
68
+ try {
69
+ if (!fs.existsSync(filePath)) return null
70
+ return JSON.parse(fs.readFileSync(filePath, 'utf8'))
71
+ } catch {
72
+ return null
73
+ }
74
+ }
75
+
76
+ export function tokenStillValid(token) {
77
+ if (!token || typeof token !== 'string') return false
78
+ try {
79
+ const parts = String(token).split('.')
80
+ if (parts.length >= 2) {
81
+ const payload = JSON.parse(Buffer.from(parts[1], 'base64url').toString('utf8'))
82
+ if (payload.exp && Date.now() / 1000 > payload.exp - 3600) return false
83
+ }
84
+ return true
85
+ } catch {
86
+ return true
87
+ }
88
+ }
89
+
90
+ export function readCachedToken(filePath) {
91
+ const raw = readTokenFile(filePath)
92
+ if (!raw || !raw.token) return null
93
+ if (!tokenStillValid(raw.token)) return null
94
+ return raw.token
95
+ }
96
+
97
+ export function writeCachedToken(filePath, token, meta) {
98
+ const data = {
99
+ token,
100
+ saved_at: new Date().toISOString(),
101
+ username: meta && meta.username ? meta.username : undefined,
102
+ }
103
+ if (meta && meta.user_center_access_token) {
104
+ data.user_center_access_token = meta.user_center_access_token
105
+ }
106
+ if (meta && meta.source) data.source = meta.source
107
+ fs.writeFileSync(filePath, JSON.stringify(data, null, 2), { mode: 0o600 })
108
+ }
109
+
110
+ export function clearCachedToken(filePath) {
111
+ try {
112
+ if (fs.existsSync(filePath)) fs.unlinkSync(filePath)
113
+ } catch {
114
+ /* ignore */
115
+ }
116
+ }
117
+
118
+ export function clearAllTokens() {
119
+ clearCachedToken(TOKEN_QA_PATH)
120
+ clearCachedToken(TOKEN_KMS_PATH)
121
+ }
package/package.json CHANGED
@@ -1,6 +1,38 @@
1
1
  {
2
2
  "name": "dsh-enterprise-knowledge-steward",
3
- "version": "0.0.0-stage",
4
- "stub": true,
5
- "description": "Temporary package placeholder for staged publishing"
6
- }
3
+ "version": "1.0.0",
4
+ "description": "企业知识管家:制度问答、清分数据、人员任职与信访台账",
5
+ "type": "module",
6
+ "main": "index.js",
7
+ "license": "UNLICENSED",
8
+ "files": [
9
+ "index.js",
10
+ "cordis.patch.yml",
11
+ "lib",
12
+ ".env.example",
13
+ "README.md"
14
+ ],
15
+ "dsh": {
16
+ "bundle": {
17
+ "patch": "./cordis.patch.yml"
18
+ }
19
+ },
20
+ "engines": {
21
+ "dsh": ">=0.1.0-rc.0"
22
+ },
23
+ "dependencies": {
24
+ "sm-crypto": "^0.3.13"
25
+ },
26
+ "peerDependencies": {
27
+ "@deepseek-ai/dsh-tools": ">=0.1.0-rc.0",
28
+ "@deepseek-ai/cordis": "*"
29
+ },
30
+ "publishConfig": {
31
+ "access": "public"
32
+ },
33
+ "keywords": [
34
+ "dsh-plugin",
35
+ "企业知识管家",
36
+ "deepseek-harness"
37
+ ]
38
+ }