@huaqiu/dsh-artifacts 0.3.7 → 0.3.8

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/lib/index.d.mts CHANGED
@@ -1,4 +1,5 @@
1
1
  import { ArtifactMeta, ArtifactType, CreateArtifactInput, CreateArtifactResult, HuaqiuArtifactService, HuaqiuArtifacts } from "./service.mjs";
2
+ import { EditorType, PLACEABLE_ARTIFACT_TYPES, PlaceArtifactRequest, PlaceableArtifactType, canPlaceArtifact, parsePlaceableArtifactType } from "./placement.mjs";
2
3
  import { Context } from "@deepseek-ai/cordis";
3
4
  import { IncomingMessage, ServerResponse } from "node:http";
4
5
  //#region src/routes.d.ts
@@ -20,4 +21,4 @@ declare module '@deepseek-ai/cordis' {
20
21
  }
21
22
  declare function apply(ctx: Context, config?: HuaqiuArtifactsConfig): void;
22
23
  //#endregion
23
- export { ARTIFACTS_ROUTE_PREFIX, type ArtifactMeta, type ArtifactType, type ArtifactsHandler, type CreateArtifactInput, type CreateArtifactResult, HuaqiuArtifactService, type HuaqiuArtifacts, HuaqiuArtifactsConfig, apply, createArtifactsHandler, inject, name };
24
+ export { ARTIFACTS_ROUTE_PREFIX, type ArtifactMeta, type ArtifactType, type ArtifactsHandler, type CreateArtifactInput, type CreateArtifactResult, type EditorType, HuaqiuArtifactService, type HuaqiuArtifacts, HuaqiuArtifactsConfig, PLACEABLE_ARTIFACT_TYPES, type PlaceArtifactRequest, type PlaceableArtifactType, apply, canPlaceArtifact, createArtifactsHandler, inject, name, parsePlaceableArtifactType };
package/lib/index.mjs CHANGED
@@ -1,4 +1,5 @@
1
1
  import { HuaqiuArtifactService, log } from "./service.mjs";
2
+ import { PLACEABLE_ARTIFACT_TYPES, canPlaceArtifact, parsePlaceableArtifactType } from "./placement.mjs";
2
3
  //#region src/routes.ts
3
4
  const ARTIFACTS_ROUTE_PREFIX = "/api/v1/huaqiu/artifacts";
4
5
  function sendJson(res, status, body) {
@@ -96,4 +97,4 @@ function apply(ctx, config = {}) {
96
97
  });
97
98
  }
98
99
  //#endregion
99
- export { ARTIFACTS_ROUTE_PREFIX, HuaqiuArtifactService, apply, createArtifactsHandler, inject, name };
100
+ export { ARTIFACTS_ROUTE_PREFIX, HuaqiuArtifactService, PLACEABLE_ARTIFACT_TYPES, apply, canPlaceArtifact, createArtifactsHandler, inject, name, parsePlaceableArtifactType };
@@ -0,0 +1,82 @@
1
+ //#region src/placement.d.ts
2
+ /**
3
+ * Placement compatibility — shared by every generation tool that can produce
4
+ * a placeable EDA artifact (`component-gen-app`, `dsh-tool-schematic-gen`,
5
+ * `dsh-tool-symbol-footprint`).
6
+ *
7
+ * This is the **frontend** copy of the rule. It exists purely to decide whether
8
+ * to *render* the `Place` action. HQ Edge enforces the same matrix
9
+ * server-side, so this is never a security boundary — it is UX only.
10
+ *
11
+ * Deliberately NOT imported from HQ Edge: the frontend package owns its own
12
+ * compatibility helper (see the task's Phase 4 notes). The two matrices must
13
+ * stay in sync, so `canPlaceArtifact` is covered by a full-matrix test here.
14
+ *
15
+ * @module
16
+ */
17
+ /** Artifact classes that can be placed into an EDA editor. */
18
+ declare const PLACEABLE_ARTIFACT_TYPES: readonly ["schematic", "symbol", "footprint"];
19
+ type PlaceableArtifactType = (typeof PLACEABLE_ARTIFACT_TYPES)[number];
20
+ /**
21
+ * Editor contexts an HQ Edge instance may be serving.
22
+ * Mirrors HQ Edge's `EditorType` (`config/hostContext.ts`).
23
+ */
24
+ type EditorType = 'sch' | 'pcb' | 'symbol' | 'footprint' | 'generic';
25
+ /**
26
+ * Can `artifactType` be placed into the editor identified by `editorType`?
27
+ *
28
+ * Fails closed: an unrecognized artifact or editor type yields `false`.
29
+ */
30
+ declare function canPlaceArtifact(artifactType: PlaceableArtifactType, editorType: EditorType): boolean;
31
+ /**
32
+ * Coerce an unknown value to a {@link PlaceableArtifactType}, or `null` when
33
+ * it is not placeable (`pcb` and `zip` artifacts have no placement support).
34
+ */
35
+ declare function parsePlaceableArtifactType(value: unknown): PlaceableArtifactType | null;
36
+ /**
37
+ * The placement request the frontend sends to HQ Edge (via edge-bridge).
38
+ *
39
+ * It carries only *what* to place — never an editor type, target host, or EDA
40
+ * URL: HQ Edge owns all of that.
41
+ */
42
+ interface PlaceArtifactRequest {
43
+ type: PlaceableArtifactType;
44
+ /** HQ Edge-resolvable URI — from `huaqiuArtifacts.getDownloadUri(id)`. */
45
+ artifactUri: string;
46
+ /** Original filename, when the URI path is opaque (e.g. `content`). */
47
+ filename?: string;
48
+ /** Root schematic filename — overrides the zip-derived root (system designs). */
49
+ root?: string;
50
+ }
51
+ /** Structural view of the `hqEdge` CLIENT service (edge-bridge browser half). */
52
+ interface HqEdgePlaceLike {
53
+ context?: {
54
+ getCurrent?(): {
55
+ targetHost?: string;
56
+ editorType?: string;
57
+ } | null;
58
+ getTargetHost?(): string;
59
+ getEditorType?(): string;
60
+ };
61
+ placeArtifact?(request: PlaceArtifactRequest): Promise<unknown>;
62
+ }
63
+ /** Everything a card needs to render + fire the Place action, or null. */
64
+ interface PlaceSupport {
65
+ editorType: string;
66
+ place(request: PlaceArtifactRequest): Promise<unknown>;
67
+ }
68
+ /**
69
+ * Build the Place support for `artifactType` from a lazily-resolved `hqEdge`.
70
+ *
71
+ * Returns null when `getHqEdge` is unavailable (standalone DSH has no
72
+ * edge-bridge, hence no `hqEdge` service). Otherwise returns a GETTER so
73
+ * callers resolve the service lazily — plugin load order and HQ Edge restarts
74
+ * both resolve correctly at click time. The getter yields null when
75
+ * placement is unavailable or the current editor cannot accept the artifact
76
+ * type (the card hides the button in that case).
77
+ *
78
+ * Editor gating is UX-only; HQ Edge re-enforces the matrix server-side (409).
79
+ */
80
+ declare function placeSupportOf(getHqEdge: (() => HqEdgePlaceLike | undefined) | undefined, artifactType: PlaceableArtifactType): (() => PlaceSupport | null) | null;
81
+ //#endregion
82
+ export { EditorType, HqEdgePlaceLike, PLACEABLE_ARTIFACT_TYPES, PlaceArtifactRequest, PlaceSupport, PlaceableArtifactType, canPlaceArtifact, parsePlaceableArtifactType, placeSupportOf };
@@ -0,0 +1,93 @@
1
+ //#region src/placement.ts
2
+ /**
3
+ * Placement compatibility — shared by every generation tool that can produce
4
+ * a placeable EDA artifact (`component-gen-app`, `dsh-tool-schematic-gen`,
5
+ * `dsh-tool-symbol-footprint`).
6
+ *
7
+ * This is the **frontend** copy of the rule. It exists purely to decide whether
8
+ * to *render* the `Place` action. HQ Edge enforces the same matrix
9
+ * server-side, so this is never a security boundary — it is UX only.
10
+ *
11
+ * Deliberately NOT imported from HQ Edge: the frontend package owns its own
12
+ * compatibility helper (see the task's Phase 4 notes). The two matrices must
13
+ * stay in sync, so `canPlaceArtifact` is covered by a full-matrix test here.
14
+ *
15
+ * @module
16
+ */
17
+ /** Artifact classes that can be placed into an EDA editor. */
18
+ const PLACEABLE_ARTIFACT_TYPES = [
19
+ "schematic",
20
+ "symbol",
21
+ "footprint"
22
+ ];
23
+ /**
24
+ * Editors each artifact type may be placed into.
25
+ *
26
+ * | Artifact | sch | pcb | symbol | footprint | generic |
27
+ * |-----------|-----|-----|--------|-----------|---------|
28
+ * | schematic | yes | no | no | no | no |
29
+ * | symbol | yes | no | yes | no | no |
30
+ * | footprint | no | yes | no | yes | no |
31
+ *
32
+ * `generic` appears in no row: HQ Edge may be inside *some* EDA host without
33
+ * knowing which editor, so placement must fail closed.
34
+ */
35
+ const COMPATIBLE_EDITORS = {
36
+ schematic: /* @__PURE__ */ new Set(["sch"]),
37
+ symbol: /* @__PURE__ */ new Set(["sch", "symbol"]),
38
+ footprint: /* @__PURE__ */ new Set(["pcb", "footprint"])
39
+ };
40
+ /**
41
+ * Can `artifactType` be placed into the editor identified by `editorType`?
42
+ *
43
+ * Fails closed: an unrecognized artifact or editor type yields `false`.
44
+ */
45
+ function canPlaceArtifact(artifactType, editorType) {
46
+ const allowed = COMPATIBLE_EDITORS[artifactType];
47
+ return allowed ? allowed.has(editorType) : false;
48
+ }
49
+ /**
50
+ * Coerce an unknown value to a {@link PlaceableArtifactType}, or `null` when
51
+ * it is not placeable (`pcb` and `zip` artifacts have no placement support).
52
+ */
53
+ function parsePlaceableArtifactType(value) {
54
+ if (typeof value !== "string") return null;
55
+ return PLACEABLE_ARTIFACT_TYPES.includes(value) ? value : null;
56
+ }
57
+ /**
58
+ * Build the Place support for `artifactType` from a lazily-resolved `hqEdge`.
59
+ *
60
+ * Returns null when `getHqEdge` is unavailable (standalone DSH has no
61
+ * edge-bridge, hence no `hqEdge` service). Otherwise returns a GETTER so
62
+ * callers resolve the service lazily — plugin load order and HQ Edge restarts
63
+ * both resolve correctly at click time. The getter yields null when
64
+ * placement is unavailable or the current editor cannot accept the artifact
65
+ * type (the card hides the button in that case).
66
+ *
67
+ * Editor gating is UX-only; HQ Edge re-enforces the matrix server-side (409).
68
+ */
69
+ function placeSupportOf(getHqEdge, artifactType) {
70
+ if (typeof getHqEdge !== "function") return null;
71
+ return () => {
72
+ let hqEdge;
73
+ try {
74
+ hqEdge = getHqEdge();
75
+ } catch {
76
+ return null;
77
+ }
78
+ if (!hqEdge || typeof hqEdge.placeArtifact !== "function") return null;
79
+ let editorType = "";
80
+ try {
81
+ editorType = hqEdge.context?.getEditorType?.() ?? hqEdge.context?.getCurrent?.()?.editorType ?? "";
82
+ } catch {
83
+ return null;
84
+ }
85
+ if (!editorType || !canPlaceArtifact(artifactType, editorType)) return null;
86
+ return {
87
+ editorType,
88
+ place: (request) => hqEdge.placeArtifact(request)
89
+ };
90
+ };
91
+ }
92
+ //#endregion
93
+ export { PLACEABLE_ARTIFACT_TYPES, canPlaceArtifact, parsePlaceableArtifactType, placeSupportOf };
package/lib/service.d.mts CHANGED
@@ -37,6 +37,18 @@ interface HuaqiuArtifacts {
37
37
  create(input: CreateArtifactInput): Promise<CreateArtifactResult>;
38
38
  get(id: string): Promise<ArtifactMeta | null>;
39
39
  readContent(id: string): Promise<Uint8Array | null>;
40
+ /**
41
+ * Stable URI for the artifact's bytes, suitable for handing to ANOTHER
42
+ * process (HQ Edge) — currently a `file://` URI to the stored content.
43
+ *
44
+ * An artifact **id** is store-local and must never cross that boundary: the
45
+ * DSH artifact store and the HQ Edge artifact store are separate, so an id
46
+ * minted here is meaningless there. Callers that need to reference an
47
+ * artifact across processes (e.g. the Place action) must pass this URI.
48
+ *
49
+ * @returns the URI, or null when the artifact is missing/expired.
50
+ */
51
+ getDownloadUri(id: string): Promise<string | null>;
40
52
  delete(id: string): Promise<void>;
41
53
  deleteAll(opts?: {
42
54
  onlyExpired?: boolean;
@@ -53,6 +65,17 @@ declare class HuaqiuArtifactService implements HuaqiuArtifacts {
53
65
  get(id: string): Promise<ArtifactMeta | null>;
54
66
  /** Returns `null` when missing or expired (miss signal for the HTTP 404). */
55
67
  readContent(id: string): Promise<Uint8Array | null>;
68
+ /**
69
+ * @see HuaqiuArtifacts.getDownloadUri
70
+ *
71
+ * Returns a `file://` URI rather than an HTTP URL: this package has no
72
+ * knowledge of the DSH origin/port it will eventually be served from, and
73
+ * HQ Edge runs on the same host, so a filesystem URI is the narrowest
74
+ * boundary that is actually resolvable on the other side. HQ Edge also
75
+ * accepts `http(s)://` URIs, so this can be upgraded without a contract
76
+ * change.
77
+ */
78
+ getDownloadUri(id: string): Promise<string | null>;
56
79
  delete(id: string): Promise<void>;
57
80
  deleteAll(opts?: {
58
81
  onlyExpired?: boolean;
package/lib/service.mjs CHANGED
@@ -1,5 +1,6 @@
1
1
  import * as fs from "node:fs";
2
2
  import * as path from "node:path";
3
+ import { pathToFileURL } from "node:url";
3
4
  import { randomUUID } from "node:crypto";
4
5
  import { dshHomePath } from "@deepseek-ai/dsh-home-paths";
5
6
  //#region src/service.ts
@@ -137,6 +138,28 @@ var HuaqiuArtifactService = class {
137
138
  return null;
138
139
  }
139
140
  }
141
+ /**
142
+ * @see HuaqiuArtifacts.getDownloadUri
143
+ *
144
+ * Returns a `file://` URI rather than an HTTP URL: this package has no
145
+ * knowledge of the DSH origin/port it will eventually be served from, and
146
+ * HQ Edge runs on the same host, so a filesystem URI is the narrowest
147
+ * boundary that is actually resolvable on the other side. HQ Edge also
148
+ * accepts `http(s)://` URIs, so this can be upgraded without a contract
149
+ * change.
150
+ */
151
+ async getDownloadUri(id) {
152
+ if (!await this.get(id)) return null;
153
+ try {
154
+ return pathToFileURL(path.join(this.artifactDir(id), "content")).href;
155
+ } catch (err) {
156
+ log("warn", "getDownloadUri failed", {
157
+ id,
158
+ err
159
+ });
160
+ return null;
161
+ }
162
+ }
140
163
  async delete(id) {
141
164
  if (typeof id !== "string" || !ARTIFACT_ID_PATTERN.test(id)) return;
142
165
  try {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@huaqiu/dsh-artifacts",
3
- "version": "0.3.7",
3
+ "version": "0.3.8",
4
4
  "type": "module",
5
5
  "main": "./lib/index.mjs",
6
6
  "types": "./lib/index.d.mts",
@@ -9,6 +9,10 @@
9
9
  "types": "./lib/index.d.mts",
10
10
  "default": "./lib/index.mjs"
11
11
  },
12
+ "./placement": {
13
+ "types": "./lib/placement.d.mts",
14
+ "default": "./lib/placement.mjs"
15
+ },
12
16
  "./service": {
13
17
  "types": "./lib/service.d.mts",
14
18
  "default": "./lib/service.mjs"
package/src/index.ts CHANGED
@@ -20,6 +20,17 @@ export type {
20
20
  export { HuaqiuArtifactService } from './service.js'
21
21
  export { ARTIFACTS_ROUTE_PREFIX, createArtifactsHandler, type ArtifactsHandler } from './routes.js'
22
22
 
23
+ // Placement compatibility — shared by the generation tools so none of them
24
+ // re-derives the artifact × editor matrix on its own.
25
+ export {
26
+ PLACEABLE_ARTIFACT_TYPES,
27
+ canPlaceArtifact,
28
+ parsePlaceableArtifactType,
29
+ type PlaceableArtifactType,
30
+ type EditorType,
31
+ type PlaceArtifactRequest,
32
+ } from './placement.js'
33
+
23
34
  export const name = '@huaqiu/dsh-artifacts'
24
35
  export const inject = ['webServer'] as const
25
36
 
@@ -0,0 +1,141 @@
1
+ /**
2
+ * Placement compatibility — shared by every generation tool that can produce
3
+ * a placeable EDA artifact (`component-gen-app`, `dsh-tool-schematic-gen`,
4
+ * `dsh-tool-symbol-footprint`).
5
+ *
6
+ * This is the **frontend** copy of the rule. It exists purely to decide whether
7
+ * to *render* the `Place` action. HQ Edge enforces the same matrix
8
+ * server-side, so this is never a security boundary — it is UX only.
9
+ *
10
+ * Deliberately NOT imported from HQ Edge: the frontend package owns its own
11
+ * compatibility helper (see the task's Phase 4 notes). The two matrices must
12
+ * stay in sync, so `canPlaceArtifact` is covered by a full-matrix test here.
13
+ *
14
+ * @module
15
+ */
16
+
17
+ /** Artifact classes that can be placed into an EDA editor. */
18
+ export const PLACEABLE_ARTIFACT_TYPES = ['schematic', 'symbol', 'footprint'] as const
19
+
20
+ export type PlaceableArtifactType = (typeof PLACEABLE_ARTIFACT_TYPES)[number]
21
+
22
+ /**
23
+ * Editor contexts an HQ Edge instance may be serving.
24
+ * Mirrors HQ Edge's `EditorType` (`config/hostContext.ts`).
25
+ */
26
+ export type EditorType = 'sch' | 'pcb' | 'symbol' | 'footprint' | 'generic'
27
+
28
+ /**
29
+ * Editors each artifact type may be placed into.
30
+ *
31
+ * | Artifact | sch | pcb | symbol | footprint | generic |
32
+ * |-----------|-----|-----|--------|-----------|---------|
33
+ * | schematic | yes | no | no | no | no |
34
+ * | symbol | yes | no | yes | no | no |
35
+ * | footprint | no | yes | no | yes | no |
36
+ *
37
+ * `generic` appears in no row: HQ Edge may be inside *some* EDA host without
38
+ * knowing which editor, so placement must fail closed.
39
+ */
40
+ const COMPATIBLE_EDITORS: Record<PlaceableArtifactType, ReadonlySet<EditorType>> = {
41
+ schematic: new Set<EditorType>(['sch']),
42
+ symbol: new Set<EditorType>(['sch', 'symbol']),
43
+ footprint: new Set<EditorType>(['pcb', 'footprint']),
44
+ }
45
+
46
+ /**
47
+ * Can `artifactType` be placed into the editor identified by `editorType`?
48
+ *
49
+ * Fails closed: an unrecognized artifact or editor type yields `false`.
50
+ */
51
+ export function canPlaceArtifact(
52
+ artifactType: PlaceableArtifactType,
53
+ editorType: EditorType,
54
+ ): boolean {
55
+ const allowed = COMPATIBLE_EDITORS[artifactType]
56
+ return allowed ? allowed.has(editorType) : false
57
+ }
58
+
59
+ /**
60
+ * Coerce an unknown value to a {@link PlaceableArtifactType}, or `null` when
61
+ * it is not placeable (`pcb` and `zip` artifacts have no placement support).
62
+ */
63
+ export function parsePlaceableArtifactType(value: unknown): PlaceableArtifactType | null {
64
+ if (typeof value !== 'string') return null
65
+ return (PLACEABLE_ARTIFACT_TYPES as readonly string[]).includes(value)
66
+ ? (value as PlaceableArtifactType)
67
+ : null
68
+ }
69
+
70
+ /**
71
+ * The placement request the frontend sends to HQ Edge (via edge-bridge).
72
+ *
73
+ * It carries only *what* to place — never an editor type, target host, or EDA
74
+ * URL: HQ Edge owns all of that.
75
+ */
76
+ export interface PlaceArtifactRequest {
77
+ type: PlaceableArtifactType
78
+ /** HQ Edge-resolvable URI — from `huaqiuArtifacts.getDownloadUri(id)`. */
79
+ artifactUri: string
80
+ /** Original filename, when the URI path is opaque (e.g. `content`). */
81
+ filename?: string
82
+ /** Root schematic filename — overrides the zip-derived root (system designs). */
83
+ root?: string
84
+ }
85
+
86
+ // ── hqEdge place seam ───────────────────────────────────────────────────────
87
+
88
+ /** Structural view of the `hqEdge` CLIENT service (edge-bridge browser half). */
89
+ export interface HqEdgePlaceLike {
90
+ context?: {
91
+ getCurrent?(): { targetHost?: string; editorType?: string } | null
92
+ getTargetHost?(): string
93
+ getEditorType?(): string
94
+ }
95
+ placeArtifact?(request: PlaceArtifactRequest): Promise<unknown>
96
+ }
97
+
98
+ /** Everything a card needs to render + fire the Place action, or null. */
99
+ export interface PlaceSupport {
100
+ editorType: string
101
+ place(request: PlaceArtifactRequest): Promise<unknown>
102
+ }
103
+
104
+ /**
105
+ * Build the Place support for `artifactType` from a lazily-resolved `hqEdge`.
106
+ *
107
+ * Returns null when `getHqEdge` is unavailable (standalone DSH has no
108
+ * edge-bridge, hence no `hqEdge` service). Otherwise returns a GETTER so
109
+ * callers resolve the service lazily — plugin load order and HQ Edge restarts
110
+ * both resolve correctly at click time. The getter yields null when
111
+ * placement is unavailable or the current editor cannot accept the artifact
112
+ * type (the card hides the button in that case).
113
+ *
114
+ * Editor gating is UX-only; HQ Edge re-enforces the matrix server-side (409).
115
+ */
116
+ export function placeSupportOf(
117
+ getHqEdge: (() => HqEdgePlaceLike | undefined) | undefined,
118
+ artifactType: PlaceableArtifactType,
119
+ ): (() => PlaceSupport | null) | null {
120
+ if (typeof getHqEdge !== 'function') return null
121
+ return () => {
122
+ let hqEdge: HqEdgePlaceLike | undefined
123
+ try {
124
+ hqEdge = getHqEdge()
125
+ } catch {
126
+ return null
127
+ }
128
+ if (!hqEdge || typeof hqEdge.placeArtifact !== 'function') return null
129
+ let editorType = ''
130
+ try {
131
+ editorType = hqEdge.context?.getEditorType?.() ?? hqEdge.context?.getCurrent?.()?.editorType ?? ''
132
+ } catch {
133
+ return null
134
+ }
135
+ if (!editorType || !canPlaceArtifact(artifactType, editorType as EditorType)) return null
136
+ return {
137
+ editorType,
138
+ place: (request) => hqEdge.placeArtifact!(request),
139
+ }
140
+ }
141
+ }
package/src/service.ts CHANGED
@@ -19,6 +19,7 @@
19
19
  */
20
20
  import * as fs from 'node:fs'
21
21
  import * as path from 'node:path'
22
+ import { pathToFileURL } from 'node:url'
22
23
  import { randomUUID } from 'node:crypto'
23
24
  import { dshHomePath } from '@deepseek-ai/dsh-home-paths'
24
25
 
@@ -67,6 +68,18 @@ export interface HuaqiuArtifacts {
67
68
  create(input: CreateArtifactInput): Promise<CreateArtifactResult>
68
69
  get(id: string): Promise<ArtifactMeta | null>
69
70
  readContent(id: string): Promise<Uint8Array | null>
71
+ /**
72
+ * Stable URI for the artifact's bytes, suitable for handing to ANOTHER
73
+ * process (HQ Edge) — currently a `file://` URI to the stored content.
74
+ *
75
+ * An artifact **id** is store-local and must never cross that boundary: the
76
+ * DSH artifact store and the HQ Edge artifact store are separate, so an id
77
+ * minted here is meaningless there. Callers that need to reference an
78
+ * artifact across processes (e.g. the Place action) must pass this URI.
79
+ *
80
+ * @returns the URI, or null when the artifact is missing/expired.
81
+ */
82
+ getDownloadUri(id: string): Promise<string | null>
70
83
  delete(id: string): Promise<void>
71
84
  deleteAll(opts?: { onlyExpired?: boolean }): Promise<number>
72
85
  }
@@ -211,6 +224,27 @@ export class HuaqiuArtifactService implements HuaqiuArtifacts {
211
224
  }
212
225
  }
213
226
 
227
+ /**
228
+ * @see HuaqiuArtifacts.getDownloadUri
229
+ *
230
+ * Returns a `file://` URI rather than an HTTP URL: this package has no
231
+ * knowledge of the DSH origin/port it will eventually be served from, and
232
+ * HQ Edge runs on the same host, so a filesystem URI is the narrowest
233
+ * boundary that is actually resolvable on the other side. HQ Edge also
234
+ * accepts `http(s)://` URIs, so this can be upgraded without a contract
235
+ * change.
236
+ */
237
+ async getDownloadUri(id: string): Promise<string | null> {
238
+ const meta = await this.get(id)
239
+ if (!meta) return null
240
+ try {
241
+ return pathToFileURL(path.join(this.artifactDir(id), 'content')).href
242
+ } catch (err) {
243
+ log('warn', 'getDownloadUri failed', { id, err })
244
+ return null
245
+ }
246
+ }
247
+
214
248
  async delete(id: string): Promise<void> {
215
249
  if (typeof id !== 'string' || !ARTIFACT_ID_PATTERN.test(id)) return
216
250
  try {