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

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,66 @@
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`(当前 **1.0.1**)
6
+
7
+ > 1.0.1 起已内置 `sm-crypto`,安装时不再向 npm 拉取依赖,可避免桌面版解析依赖 403。
8
+
9
+ ## 安装(推荐指定版本)
10
+
11
+ 桌面版:设置 → 插件 → 安装框填入:
12
+
13
+ ```
14
+ dsh-enterprise-knowledge-steward@1.0.1
15
+ ```
16
+
17
+ 若包名不被接受,粘贴 tarball:
18
+
19
+ ```
20
+ https://registry.npmjs.org/dsh-enterprise-knowledge-steward/-/dsh-enterprise-knowledge-steward-1.0.1.tgz
21
+ ```
22
+
23
+ 国内镜像:
24
+
25
+ ```
26
+ https://registry.npmmirror.com/dsh-enterprise-knowledge-steward/-/dsh-enterprise-knowledge-steward-1.0.1.tgz
27
+ ```
28
+
29
+ CLI:
30
+
31
+ ```bash
32
+ dsh plugin --profile desktop add dsh-enterprise-knowledge-steward@1.0.1
33
+ dsh plugin --profile web add dsh-enterprise-knowledge-steward@1.0.1
34
+ dsh plugin --profile dsh-tui add dsh-enterprise-knowledge-steward@1.0.1
35
+ ```
36
+
37
+ 无全局 `dsh` 时:
38
+
39
+ ```bash
40
+ npx @deepseek-ai/dsh plugin --profile desktop add dsh-enterprise-knowledge-steward@1.0.1
41
+ ```
42
+
43
+ 安装后**完全退出再打开** DSH。
44
+
45
+ ## 配置账密
46
+
47
+ 在插件安装目录复制 `.env.example` 为 `.env`,填写:
48
+
49
+ ```
50
+ KMS_USERNAME=你的账号
51
+ KMS_PASSWORD=你的密码
52
+ ```
53
+
54
+ 只需一套 50004(KMS)账密;`KMS_BASE` / `ZHDN_BASE` 一般不用改。**不要把 `.env` 提交或发布到 npm。**
55
+
56
+ ## 使用
57
+
58
+ 打开对话直接提问,例如「查一下人员任职台账」「川高公司收费清分管理办法」。
59
+ 也可点名工具:`ledger_personnel`、`zhdn_ask`、`knowledge_list` 等。
60
+
61
+ ## 本地目录 / tgz(可选)
62
+
63
+ ```bash
64
+ dsh plugin --profile desktop add ./企业知识管家-dsh-plugin
65
+ dsh plugin --profile desktop add ./dsh-enterprise-knowledge-steward-1.0.1.tgz
66
+ ```
@@ -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,180 @@
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(path.join(PLUGIN_ROOT, 'vendor', 'sm-crypto')).sm2
26
+ } catch (e) {
27
+ const err = new Error('缺少内置 sm-crypto(vendor/sm-crypto),请重新安装插件包')
28
+ err.code = 'NO_SM_CRYPTO'
29
+ err.cause = e
30
+ throw err
31
+ }
32
+ }
33
+
34
+ /** Full KMS login → { accessToken, ucToken, username } */
35
+ export async function kmsLoginFull(force = false) {
36
+ ensureEnv()
37
+ if (!force) {
38
+ const raw = readTokenFile(TOKEN_KMS_PATH)
39
+ if (raw && raw.token && tokenStillValid(raw.token) && raw.user_center_access_token) {
40
+ return {
41
+ accessToken: raw.token,
42
+ ucToken: raw.user_center_access_token,
43
+ username: raw.username || '',
44
+ }
45
+ }
46
+ }
47
+
48
+ const { username, password } = kmsCredentials()
49
+ if (!username || !password) {
50
+ const err = new Error('未配置账号:请在插件目录 .env 填写 KMS_USERNAME / KMS_PASSWORD')
51
+ err.code = 'NO_KMS_CREDENTIALS'
52
+ throw err
53
+ }
54
+
55
+ const pkRes = await fetch(`${kmsBaseUrl()}/api/search_pangu/get_public_key`, {
56
+ headers: { Accept: 'application/json' },
57
+ })
58
+ const pkBody = await pkRes.json().catch(() => ({}))
59
+ let pub = pkBody && pkBody.data
60
+ if (!pub) {
61
+ const err = new Error(`获取公钥失败 HTTP ${pkRes.status}`)
62
+ err.code = 'KMS_LOGIN_FAILED'
63
+ throw err
64
+ }
65
+ if (!String(pub).startsWith('04')) pub = '04' + pub
66
+
67
+ const digest = crypto.createHash('md5').update(String(password), 'utf8').digest('hex')
68
+ const sm2 = loadSm2()
69
+ const enc = sm2.doEncrypt(digest, pub, 1)
70
+
71
+ const loginRes = await fetch(`${kmsBaseUrl()}/api/search_pangu/login`, {
72
+ method: 'POST',
73
+ headers: { 'Content-Type': 'application/json', Accept: 'application/json' },
74
+ body: JSON.stringify({ username, password: enc }),
75
+ })
76
+ const loginBody = await loginRes.json().catch(() => ({}))
77
+ const data = (loginBody && loginBody.data) || {}
78
+ const accessToken = data.access_token || data.token
79
+ const ucToken = data.user_center_access_token || data.userCenterAccessToken
80
+ if (!accessToken) {
81
+ const err = new Error(
82
+ `登录失败 HTTP ${loginRes.status} code=${loginBody.code} msg=${loginBody.message || ''}`,
83
+ )
84
+ err.code = 'KMS_LOGIN_FAILED'
85
+ throw err
86
+ }
87
+ if (!ucToken) {
88
+ const err = new Error('登录成功但缺少 user_center_access_token')
89
+ err.code = 'KMS_LOGIN_FAILED'
90
+ throw err
91
+ }
92
+
93
+ writeCachedToken(TOKEN_KMS_PATH, accessToken, {
94
+ username,
95
+ user_center_access_token: ucToken,
96
+ })
97
+ return { accessToken, ucToken, username }
98
+ }
99
+
100
+ export async function kmsLogin(force = false) {
101
+ const full = await kmsLoginFull(!!force)
102
+ return full.accessToken
103
+ }
104
+
105
+ export async function exchangeQaToken(ucToken) {
106
+ const url = `${baseUrl()}/api/admin/user/self`
107
+ const res = await fetch(url, {
108
+ method: 'GET',
109
+ headers: { Authorization: ucToken, Accept: 'application/json' },
110
+ })
111
+ const body = await res.json().catch(() => ({}))
112
+ if (!res.ok || body.code !== 0) {
113
+ const err = new Error(
114
+ `会话交换失败 HTTP ${res.status} code=${body.code} msg=${body.message || ''}`,
115
+ )
116
+ err.code = 'QA_EXCHANGE_FAILED'
117
+ throw err
118
+ }
119
+ const t = body.data && body.data.token
120
+ if (typeof t === 'string' && t.length > 20) {
121
+ return { qaToken: t, source: 'self.data.token', user: body.data }
122
+ }
123
+ return { qaToken: ucToken, source: 'user_center_access_token', user: body.data }
124
+ }
125
+
126
+ export async function login(force = false) {
127
+ if (!force) {
128
+ const cached = readCachedToken(TOKEN_QA_PATH)
129
+ if (cached) return cached
130
+ }
131
+ const { ucToken, username } = await kmsLoginFull(!!force)
132
+ const { qaToken, source } = await exchangeQaToken(ucToken)
133
+ writeCachedToken(TOKEN_QA_PATH, qaToken, { username, source })
134
+ return qaToken
135
+ }
136
+
137
+ export async function authHeader(forceLogin = false) {
138
+ const token = await login(!!forceLogin)
139
+ return { Authorization: token }
140
+ }
141
+
142
+ export async function kmsAuthHeader(forceLogin = false) {
143
+ const token = await kmsLogin(!!forceLogin)
144
+ return { Authorization: 'Bearer ' + token }
145
+ }
146
+
147
+ export async function withAuth(doRequest) {
148
+ let headers = await authHeader(false)
149
+ let res = await doRequest(headers)
150
+ if (res.status === 401) {
151
+ clearCachedToken(TOKEN_QA_PATH)
152
+ clearCachedToken(TOKEN_KMS_PATH)
153
+ headers = await authHeader(true)
154
+ res = await doRequest(headers)
155
+ }
156
+ return res
157
+ }
158
+
159
+ export async function withKmsAuth(doRequest) {
160
+ let headers = await kmsAuthHeader(false)
161
+ let res = await doRequest(headers)
162
+ try {
163
+ const bodyText = await res.clone().text()
164
+ const j = JSON.parse(bodyText)
165
+ if (j && (j.code === 401 || j.status === 401)) {
166
+ clearCachedToken(TOKEN_KMS_PATH)
167
+ headers = await kmsAuthHeader(true)
168
+ res = await doRequest(headers)
169
+ return res
170
+ }
171
+ } catch {
172
+ /* ignore */
173
+ }
174
+ if (res.status === 401) {
175
+ clearCachedToken(TOKEN_KMS_PATH)
176
+ headers = await kmsAuthHeader(true)
177
+ res = await doRequest(headers)
178
+ }
179
+ return res
180
+ }