@huaqiu/dsh-tool-plugin-update 0.0.0-stage → 0.5.22
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/LICENSE +21 -0
- package/cordis.patch.yml +10 -0
- package/lib/client.js +1869 -0
- package/lib/client.js.map +1 -0
- package/lib/index.d.mts +32 -0
- package/lib/index.mjs +178 -0
- package/package.json +50 -3
- package/src/client/api.ts +98 -0
- package/src/client/copy.ts +120 -0
- package/src/client/icons.ts +91 -0
- package/src/client/index.ts +1052 -0
- package/src/client/update-view.ts +619 -0
- package/src/index.ts +108 -0
- package/src/routes.ts +149 -0
- package/README.md +0 -3
package/src/index.ts
ADDED
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Huaqiu EDA plugin-update DSH plugin (node half) — `@huaqiu/dsh-tool-plugin-update`.
|
|
3
|
+
*
|
|
4
|
+
* Thin adapter between the DSH Web update UI and the hq-edge plugin-update
|
|
5
|
+
* API (task dsh-plugin-discovery-and-update-with-hq-edge-update-reporting.md
|
|
6
|
+
* §7). The browser half mounts the sidebar toolbutton (badge = actionable
|
|
7
|
+
* count) and the review/confirm/ignore view; every request it makes lands on
|
|
8
|
+
* this package's same-origin prefix routes, which forward to hq-edge.
|
|
9
|
+
*
|
|
10
|
+
* ── Architectural boundary ─────────────────────────────────────────────────
|
|
11
|
+
* All update logic is owned ONCE by hq-edge (discovery, compatibility,
|
|
12
|
+
* ignore persistence, the install transaction with cross-process lock and
|
|
13
|
+
* atomic activation). This plugin is a thin adapter: no manifest fetching, no
|
|
14
|
+
* registry calls, no download/verify code, no `@hqedge/*` dependency. It only
|
|
15
|
+
* talks to hq-edge through the edge-bridge (`ctx.hqEdge.baseUrl`), matching
|
|
16
|
+
* the `@huaqiu/dsh-tool-quote` / `@huaqiu/dsh-eda-host` convention.
|
|
17
|
+
*
|
|
18
|
+
* `hqEdge` is a REQUIRED inject: without the bridge the update UI reports
|
|
19
|
+
* "hq-edge is not reachable" rather than showing stale data.
|
|
20
|
+
*
|
|
21
|
+
* @module @huaqiu/dsh-tool-plugin-update
|
|
22
|
+
*/
|
|
23
|
+
|
|
24
|
+
import type { Context } from '@deepseek-ai/cordis'
|
|
25
|
+
import type {} from '@deepseek-ai/dsh-host-webserver'
|
|
26
|
+
import { getLogger, type PluginLogger } from '@huaqiu/dsh-plugin-log'
|
|
27
|
+
import { createUpdateForwarder, UPDATE_ROUTE_PREFIX } from './routes.js'
|
|
28
|
+
|
|
29
|
+
/** Plugin id — matches package.json. */
|
|
30
|
+
export const name = '@huaqiu/dsh-tool-plugin-update'
|
|
31
|
+
|
|
32
|
+
/** Cordis services this half depends on: hq-edge bridge (REQUIRED) + the DSH
|
|
33
|
+
* webServer surface that hosts the same-origin forwarding routes. */
|
|
34
|
+
export const inject = ['hqEdge', 'webServer'] as const
|
|
35
|
+
|
|
36
|
+
declare module '@deepseek-ai/cordis' {
|
|
37
|
+
interface Context {
|
|
38
|
+
/**
|
|
39
|
+
* Provided by the hq-edge `edge-bridge` plugin (the HOST). `baseUrl` is
|
|
40
|
+
* the loopback HQ Edge endpoint, e.g. "http://localhost:18080". Read
|
|
41
|
+
* lazily on every request.
|
|
42
|
+
*/
|
|
43
|
+
hqEdge: { baseUrl?: string }
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/** Shared component name for the unified DSH-plugin log. */
|
|
48
|
+
const COMPONENT = 'dsh-tool-plugin-update'
|
|
49
|
+
|
|
50
|
+
let _log: PluginLogger | null = null
|
|
51
|
+
function log(): PluginLogger {
|
|
52
|
+
if (_log === null) _log = getLogger(COMPONENT)
|
|
53
|
+
return _log
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Host plugin body — register the forwarding routes.
|
|
58
|
+
*
|
|
59
|
+
* The HQ Edge endpoint is resolved lazily per request from `ctx.hqEdge`
|
|
60
|
+
* (edge-bridge). When the bridge is absent the plugin refuses to load: an
|
|
61
|
+
* update UI without a host would only mislead.
|
|
62
|
+
*
|
|
63
|
+
* @param ctx - real cordis context (node side).
|
|
64
|
+
* @returns disposer — unregisters the routes on plugin dispose.
|
|
65
|
+
*/
|
|
66
|
+
export function apply(ctx: Context): () => void {
|
|
67
|
+
const hq = ctx.hqEdge
|
|
68
|
+
if (!hq || typeof hq.baseUrl !== 'string' || hq.baseUrl.trim().length === 0) {
|
|
69
|
+
throw new Error(
|
|
70
|
+
'@huaqiu/dsh-tool-plugin-update requires a usable hq-edge context: the ' +
|
|
71
|
+
'edge-bridge plugin did not provide ctx.hqEdge.baseUrl. The update UI ' +
|
|
72
|
+
'cannot work without hq-edge — check that the bridge started.',
|
|
73
|
+
)
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
const getHqEdgeBaseUrl = (): string | undefined => {
|
|
77
|
+
const current = ctx.hqEdge
|
|
78
|
+
return current?.baseUrl && current.baseUrl.trim().length > 0 ? current.baseUrl : undefined
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
const disposers: Array<() => void> = []
|
|
82
|
+
if (ctx.webServer && typeof ctx.webServer.register === 'function') {
|
|
83
|
+
ctx.effect(() =>
|
|
84
|
+
ctx.webServer.register({
|
|
85
|
+
kind: 'prefix',
|
|
86
|
+
path: UPDATE_ROUTE_PREFIX,
|
|
87
|
+
handler: createUpdateForwarder(getHqEdgeBaseUrl),
|
|
88
|
+
}),
|
|
89
|
+
)
|
|
90
|
+
} else {
|
|
91
|
+
log().warn('webServer unavailable — plugin-update routes are not exposed; the sidebar UI cannot load data')
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
log().info('applying dsh-tool-plugin-update node half', {
|
|
95
|
+
routePrefix: UPDATE_ROUTE_PREFIX,
|
|
96
|
+
hqEdgeBaseUrl: getHqEdgeBaseUrl() ?? null,
|
|
97
|
+
})
|
|
98
|
+
|
|
99
|
+
return () => {
|
|
100
|
+
for (const dispose of disposers) {
|
|
101
|
+
try {
|
|
102
|
+
dispose()
|
|
103
|
+
} catch {
|
|
104
|
+
// best-effort teardown
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
}
|
package/src/routes.ts
ADDED
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Same-origin forwarding routes for the plugin-update UI (browser half).
|
|
3
|
+
*
|
|
4
|
+
* Browser → DSH webServer prefix route `/api/v1/huaqiu/plugin-update/…` →
|
|
5
|
+
* this handler → hq-edge loopback HTTP API `/api/v1/plugin-updates/…`.
|
|
6
|
+
*
|
|
7
|
+
* The forwarding is a THIN adapter: hq-edge owns discovery, compatibility,
|
|
8
|
+
* ignore persistence and the install transaction (with its loopback-only
|
|
9
|
+
* protection — every request below originates from this node process on the
|
|
10
|
+
* same host, so the loopback guard is satisfied naturally). This plugin
|
|
11
|
+
* carries no update logic and no `@hqedge/*` dependency.
|
|
12
|
+
*
|
|
13
|
+
* Routes (mirroring hq-edge apps/server/src/routes/plugin-updates.ts):
|
|
14
|
+
* GET /plugin-update/report → GET /api/v1/plugin-updates
|
|
15
|
+
* POST /plugin-update/check → POST /api/v1/plugin-updates/check
|
|
16
|
+
* POST /plugin-update/install → POST /api/v1/plugin-updates/install {setVersion}
|
|
17
|
+
* POST /plugin-update/ignore → POST /api/v1/plugin-updates/ignore {setVersion}
|
|
18
|
+
* POST /plugin-update/unignore → POST /api/v1/plugin-updates/unignore {setVersion}
|
|
19
|
+
* GET /plugin-update/runtime → GET /api/v1/runtime-update
|
|
20
|
+
*
|
|
21
|
+
* @module
|
|
22
|
+
*/
|
|
23
|
+
import type { IncomingMessage, ServerResponse } from 'node:http'
|
|
24
|
+
|
|
25
|
+
export const UPDATE_ROUTE_PREFIX = '/api/v1/huaqiu/plugin-update'
|
|
26
|
+
|
|
27
|
+
/** End-to-end budget for one forwarded request, in milliseconds. */
|
|
28
|
+
export const DEFAULT_FORWARD_TIMEOUT_MS = 30_000
|
|
29
|
+
|
|
30
|
+
export type BaseUrlResolver = () => string | undefined
|
|
31
|
+
|
|
32
|
+
function sendJson(res: ServerResponse, status: number, body: unknown): void {
|
|
33
|
+
const payload = JSON.stringify(body)
|
|
34
|
+
res.writeHead(status, {
|
|
35
|
+
'content-type': 'application/json; charset=utf-8',
|
|
36
|
+
'cache-control': 'no-store',
|
|
37
|
+
})
|
|
38
|
+
res.end(payload)
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/** Split the sub-path off the prefix. Returns null on a bad shape. */
|
|
42
|
+
function parsePath(req: IncomingMessage): string | null {
|
|
43
|
+
const url = req.url ?? ''
|
|
44
|
+
const q = url.indexOf('?')
|
|
45
|
+
const pathname = q >= 0 ? url.slice(0, q) : url
|
|
46
|
+
if (!pathname.startsWith(UPDATE_ROUTE_PREFIX)) return null
|
|
47
|
+
return pathname.slice(UPDATE_ROUTE_PREFIX.length)
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/** Read the request body (bounded) as UTF-8 text. */
|
|
51
|
+
function readBody(req: IncomingMessage): Promise<string> {
|
|
52
|
+
return new Promise((resolve, reject) => {
|
|
53
|
+
let size = 0
|
|
54
|
+
const chunks: Buffer[] = []
|
|
55
|
+
req.on('data', (chunk: Buffer) => {
|
|
56
|
+
size += chunk.length
|
|
57
|
+
if (size > 64 * 1024) {
|
|
58
|
+
reject(new Error('request body too large'))
|
|
59
|
+
req.destroy()
|
|
60
|
+
return
|
|
61
|
+
}
|
|
62
|
+
chunks.push(chunk)
|
|
63
|
+
})
|
|
64
|
+
req.on('end', () => resolve(Buffer.concat(chunks).toString('utf8')))
|
|
65
|
+
req.on('error', reject)
|
|
66
|
+
})
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/** Map a sub-path to { method, path } on the hq-edge API. */
|
|
70
|
+
function routeFor(sub: string): { method: string; path: string } | null {
|
|
71
|
+
if (sub === '/report' || sub === '') {
|
|
72
|
+
return { method: 'GET', path: '/api/v1/plugin-updates' }
|
|
73
|
+
}
|
|
74
|
+
if (sub === '/runtime') {
|
|
75
|
+
return { method: 'GET', path: '/api/v1/runtime-update' }
|
|
76
|
+
}
|
|
77
|
+
if (sub === '/check') return { method: 'POST', path: '/api/v1/plugin-updates/check' }
|
|
78
|
+
if (sub === '/install') return { method: 'POST', path: '/api/v1/plugin-updates/install' }
|
|
79
|
+
if (sub === '/ignore') return { method: 'POST', path: '/api/v1/plugin-updates/ignore' }
|
|
80
|
+
if (sub === '/unignore') return { method: 'POST', path: '/api/v1/plugin-updates/unignore' }
|
|
81
|
+
return null
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/** Create the prefix-route handler forwarding to hq-edge. */
|
|
85
|
+
export function createUpdateForwarder(
|
|
86
|
+
baseUrlResolver: BaseUrlResolver,
|
|
87
|
+
timeoutMs: number = DEFAULT_FORWARD_TIMEOUT_MS,
|
|
88
|
+
): (req: IncomingMessage, res: ServerResponse) => Promise<void> {
|
|
89
|
+
return async (req: IncomingMessage, res: ServerResponse) => {
|
|
90
|
+
const sub = parsePath(req)
|
|
91
|
+
if (sub === null) {
|
|
92
|
+
sendJson(res, 404, { error: 'not found' })
|
|
93
|
+
return
|
|
94
|
+
}
|
|
95
|
+
const target = routeFor(sub)
|
|
96
|
+
if (target === null) {
|
|
97
|
+
sendJson(res, 404, { error: 'not found' })
|
|
98
|
+
return
|
|
99
|
+
}
|
|
100
|
+
if (req.method !== 'GET' && req.method !== 'POST' && req.method !== 'HEAD') {
|
|
101
|
+
sendJson(res, 405, { error: 'method not allowed' })
|
|
102
|
+
return
|
|
103
|
+
}
|
|
104
|
+
const base = (baseUrlResolver() ?? '').replace(/\/+$/, '')
|
|
105
|
+
if (base.length === 0) {
|
|
106
|
+
sendJson(res, 503, { error: 'hq-edge is not reachable from this plugin (no base URL)' })
|
|
107
|
+
return
|
|
108
|
+
}
|
|
109
|
+
let body = ''
|
|
110
|
+
if (req.method === 'POST') {
|
|
111
|
+
try {
|
|
112
|
+
body = await readBody(req)
|
|
113
|
+
} catch (err) {
|
|
114
|
+
sendJson(res, 400, { error: err instanceof Error ? err.message : String(err) })
|
|
115
|
+
return
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
const controller = new AbortController()
|
|
119
|
+
const timer = setTimeout(() => controller.abort(), timeoutMs)
|
|
120
|
+
try {
|
|
121
|
+
const upstream = await fetch(`${base}${target.path}`, {
|
|
122
|
+
method: target.method,
|
|
123
|
+
headers: {
|
|
124
|
+
'content-type': 'application/json; charset=utf-8',
|
|
125
|
+
'user-agent': 'hq-edge-dsh-tool-plugin-update/0.5.21',
|
|
126
|
+
},
|
|
127
|
+
...(body.length > 0 ? { body } : {}),
|
|
128
|
+
signal: controller.signal,
|
|
129
|
+
})
|
|
130
|
+
const upstreamBody = await upstream.text()
|
|
131
|
+
let json: unknown = null
|
|
132
|
+
try {
|
|
133
|
+
json = JSON.parse(upstreamBody)
|
|
134
|
+
} catch {
|
|
135
|
+
json = { raw: upstreamBody.slice(0, 512) }
|
|
136
|
+
}
|
|
137
|
+
sendJson(res, upstream.status, json)
|
|
138
|
+
} catch (err) {
|
|
139
|
+
const message = err instanceof Error ? err.message : String(err)
|
|
140
|
+
if (err instanceof Error && err.name === 'AbortError') {
|
|
141
|
+
sendJson(res, 504, { error: `hq-edge request timed out after ${timeoutMs}ms` })
|
|
142
|
+
return
|
|
143
|
+
}
|
|
144
|
+
sendJson(res, 502, { error: `hq-edge request failed: ${message}` })
|
|
145
|
+
} finally {
|
|
146
|
+
clearTimeout(timer)
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
}
|
package/README.md
DELETED