@huaqiu/dsh-artifacts 0.3.7 → 0.3.9

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,83 @@
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", "pcb"];
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 (`zip` artifacts have no placement of their own — a zip
34
+ * is expanded into schematics before the RPC).
35
+ */
36
+ declare function parsePlaceableArtifactType(value: unknown): PlaceableArtifactType | null;
37
+ /**
38
+ * The placement request the frontend sends to HQ Edge (via edge-bridge).
39
+ *
40
+ * It carries only *what* to place — never an editor type, target host, or EDA
41
+ * URL: HQ Edge owns all of that.
42
+ */
43
+ interface PlaceArtifactRequest {
44
+ type: PlaceableArtifactType;
45
+ /** HQ Edge-resolvable URI — from `huaqiuArtifacts.getDownloadUri(id)`. */
46
+ artifactUri: string;
47
+ /** Original filename, when the URI path is opaque (e.g. `content`). */
48
+ filename?: string;
49
+ /** Root schematic filename — overrides the zip-derived root (system designs). */
50
+ root?: string;
51
+ }
52
+ /** Structural view of the `hqEdge` CLIENT service (edge-bridge browser half). */
53
+ interface HqEdgePlaceLike {
54
+ context?: {
55
+ getCurrent?(): {
56
+ targetHost?: string;
57
+ editorType?: string;
58
+ } | null;
59
+ getTargetHost?(): string;
60
+ getEditorType?(): string;
61
+ };
62
+ placeArtifact?(request: PlaceArtifactRequest): Promise<unknown>;
63
+ }
64
+ /** Everything a card needs to render + fire the Place action, or null. */
65
+ interface PlaceSupport {
66
+ editorType: string;
67
+ place(request: PlaceArtifactRequest): Promise<unknown>;
68
+ }
69
+ /**
70
+ * Build the Place support for `artifactType` from a lazily-resolved `hqEdge`.
71
+ *
72
+ * Returns null when `getHqEdge` is unavailable (standalone DSH has no
73
+ * edge-bridge, hence no `hqEdge` service). Otherwise returns a GETTER so
74
+ * callers resolve the service lazily — plugin load order and HQ Edge restarts
75
+ * both resolve correctly at click time. The getter yields null when
76
+ * placement is unavailable or the current editor cannot accept the artifact
77
+ * type (the card hides the button in that case).
78
+ *
79
+ * Editor gating is UX-only; HQ Edge re-enforces the matrix server-side (409).
80
+ */
81
+ declare function placeSupportOf(getHqEdge: (() => HqEdgePlaceLike | undefined) | undefined, artifactType: PlaceableArtifactType): (() => PlaceSupport | null) | null;
82
+ //#endregion
83
+ export { EditorType, HqEdgePlaceLike, PLACEABLE_ARTIFACT_TYPES, PlaceArtifactRequest, PlaceSupport, PlaceableArtifactType, canPlaceArtifact, parsePlaceableArtifactType, placeSupportOf };
@@ -0,0 +1,110 @@
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
+ "pcb"
23
+ ];
24
+ /**
25
+ * Editors each artifact type may be placed into.
26
+ *
27
+ * | Artifact | sch | pcb | symbol | footprint | generic |
28
+ * |-----------|-----|-----|--------|-----------|---------|
29
+ * | schematic | yes | no | no | no | yes |
30
+ * | symbol | yes | no | yes | no | yes |
31
+ * | footprint | no | yes | no | yes | yes |
32
+ * | pcb | no | yes | no | no | yes |
33
+ *
34
+ * `pcb` rides the same design-block RPC as schematics (`DesignContent.pcb`):
35
+ * the editor-side PCB placement flow is the placement team's work and builds
36
+ * on this channel, so it must not be assumed away. `generic` means the host
37
+ * did not declare a specific editor context, and per the host contract such a
38
+ * deployment supports ALL placement targets by design — the EDA host is the
39
+ * final authority (HQ Edge re-enforces the same matrix server-side). Unknown
40
+ * editor values still fail closed.
41
+ */
42
+ const COMPATIBLE_EDITORS = {
43
+ schematic: /* @__PURE__ */ new Set(["sch", "generic"]),
44
+ symbol: /* @__PURE__ */ new Set([
45
+ "sch",
46
+ "symbol",
47
+ "generic"
48
+ ]),
49
+ footprint: /* @__PURE__ */ new Set([
50
+ "pcb",
51
+ "footprint",
52
+ "generic"
53
+ ]),
54
+ pcb: /* @__PURE__ */ new Set(["pcb", "generic"])
55
+ };
56
+ /**
57
+ * Can `artifactType` be placed into the editor identified by `editorType`?
58
+ *
59
+ * Fails closed: an unrecognized artifact or editor type yields `false`.
60
+ */
61
+ function canPlaceArtifact(artifactType, editorType) {
62
+ const allowed = COMPATIBLE_EDITORS[artifactType];
63
+ return allowed ? allowed.has(editorType) : false;
64
+ }
65
+ /**
66
+ * Coerce an unknown value to a {@link PlaceableArtifactType}, or `null` when
67
+ * it is not placeable (`zip` artifacts have no placement of their own — a zip
68
+ * is expanded into schematics before the RPC).
69
+ */
70
+ function parsePlaceableArtifactType(value) {
71
+ if (typeof value !== "string") return null;
72
+ return PLACEABLE_ARTIFACT_TYPES.includes(value) ? value : null;
73
+ }
74
+ /**
75
+ * Build the Place support for `artifactType` from a lazily-resolved `hqEdge`.
76
+ *
77
+ * Returns null when `getHqEdge` is unavailable (standalone DSH has no
78
+ * edge-bridge, hence no `hqEdge` service). Otherwise returns a GETTER so
79
+ * callers resolve the service lazily — plugin load order and HQ Edge restarts
80
+ * both resolve correctly at click time. The getter yields null when
81
+ * placement is unavailable or the current editor cannot accept the artifact
82
+ * type (the card hides the button in that case).
83
+ *
84
+ * Editor gating is UX-only; HQ Edge re-enforces the matrix server-side (409).
85
+ */
86
+ function placeSupportOf(getHqEdge, artifactType) {
87
+ if (typeof getHqEdge !== "function") return null;
88
+ return () => {
89
+ let hqEdge;
90
+ try {
91
+ hqEdge = getHqEdge();
92
+ } catch {
93
+ return null;
94
+ }
95
+ if (!hqEdge || typeof hqEdge.placeArtifact !== "function") return null;
96
+ let editorType = "";
97
+ try {
98
+ editorType = hqEdge.context?.getEditorType?.() ?? hqEdge.context?.getCurrent?.()?.editorType ?? "";
99
+ } catch {
100
+ return null;
101
+ }
102
+ if (!editorType || !canPlaceArtifact(artifactType, editorType)) return null;
103
+ return {
104
+ editorType,
105
+ place: (request) => hqEdge.placeArtifact(request)
106
+ };
107
+ };
108
+ }
109
+ //#endregion
110
+ 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.9",
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,149 @@
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', 'pcb'] 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 | yes |
34
+ * | symbol | yes | no | yes | no | yes |
35
+ * | footprint | no | yes | no | yes | yes |
36
+ * | pcb | no | yes | no | no | yes |
37
+ *
38
+ * `pcb` rides the same design-block RPC as schematics (`DesignContent.pcb`):
39
+ * the editor-side PCB placement flow is the placement team's work and builds
40
+ * on this channel, so it must not be assumed away. `generic` means the host
41
+ * did not declare a specific editor context, and per the host contract such a
42
+ * deployment supports ALL placement targets by design — the EDA host is the
43
+ * final authority (HQ Edge re-enforces the same matrix server-side). Unknown
44
+ * editor values still fail closed.
45
+ */
46
+ const COMPATIBLE_EDITORS: Record<PlaceableArtifactType, ReadonlySet<EditorType>> = {
47
+ schematic: new Set<EditorType>(['sch', 'generic']),
48
+ symbol: new Set<EditorType>(['sch', 'symbol', 'generic']),
49
+ footprint: new Set<EditorType>(['pcb', 'footprint', 'generic']),
50
+ pcb: new Set<EditorType>(['pcb', 'generic']),
51
+ }
52
+
53
+ /**
54
+ * Can `artifactType` be placed into the editor identified by `editorType`?
55
+ *
56
+ * Fails closed: an unrecognized artifact or editor type yields `false`.
57
+ */
58
+ export function canPlaceArtifact(
59
+ artifactType: PlaceableArtifactType,
60
+ editorType: EditorType,
61
+ ): boolean {
62
+ const allowed = COMPATIBLE_EDITORS[artifactType]
63
+ return allowed ? allowed.has(editorType) : false
64
+ }
65
+
66
+ /**
67
+ * Coerce an unknown value to a {@link PlaceableArtifactType}, or `null` when
68
+ * it is not placeable (`zip` artifacts have no placement of their own — a zip
69
+ * is expanded into schematics before the RPC).
70
+ */
71
+ export function parsePlaceableArtifactType(value: unknown): PlaceableArtifactType | null {
72
+ if (typeof value !== 'string') return null
73
+ return (PLACEABLE_ARTIFACT_TYPES as readonly string[]).includes(value)
74
+ ? (value as PlaceableArtifactType)
75
+ : null
76
+ }
77
+
78
+ /**
79
+ * The placement request the frontend sends to HQ Edge (via edge-bridge).
80
+ *
81
+ * It carries only *what* to place — never an editor type, target host, or EDA
82
+ * URL: HQ Edge owns all of that.
83
+ */
84
+ export interface PlaceArtifactRequest {
85
+ type: PlaceableArtifactType
86
+ /** HQ Edge-resolvable URI — from `huaqiuArtifacts.getDownloadUri(id)`. */
87
+ artifactUri: string
88
+ /** Original filename, when the URI path is opaque (e.g. `content`). */
89
+ filename?: string
90
+ /** Root schematic filename — overrides the zip-derived root (system designs). */
91
+ root?: string
92
+ }
93
+
94
+ // ── hqEdge place seam ───────────────────────────────────────────────────────
95
+
96
+ /** Structural view of the `hqEdge` CLIENT service (edge-bridge browser half). */
97
+ export interface HqEdgePlaceLike {
98
+ context?: {
99
+ getCurrent?(): { targetHost?: string; editorType?: string } | null
100
+ getTargetHost?(): string
101
+ getEditorType?(): string
102
+ }
103
+ placeArtifact?(request: PlaceArtifactRequest): Promise<unknown>
104
+ }
105
+
106
+ /** Everything a card needs to render + fire the Place action, or null. */
107
+ export interface PlaceSupport {
108
+ editorType: string
109
+ place(request: PlaceArtifactRequest): Promise<unknown>
110
+ }
111
+
112
+ /**
113
+ * Build the Place support for `artifactType` from a lazily-resolved `hqEdge`.
114
+ *
115
+ * Returns null when `getHqEdge` is unavailable (standalone DSH has no
116
+ * edge-bridge, hence no `hqEdge` service). Otherwise returns a GETTER so
117
+ * callers resolve the service lazily — plugin load order and HQ Edge restarts
118
+ * both resolve correctly at click time. The getter yields null when
119
+ * placement is unavailable or the current editor cannot accept the artifact
120
+ * type (the card hides the button in that case).
121
+ *
122
+ * Editor gating is UX-only; HQ Edge re-enforces the matrix server-side (409).
123
+ */
124
+ export function placeSupportOf(
125
+ getHqEdge: (() => HqEdgePlaceLike | undefined) | undefined,
126
+ artifactType: PlaceableArtifactType,
127
+ ): (() => PlaceSupport | null) | null {
128
+ if (typeof getHqEdge !== 'function') return null
129
+ return () => {
130
+ let hqEdge: HqEdgePlaceLike | undefined
131
+ try {
132
+ hqEdge = getHqEdge()
133
+ } catch {
134
+ return null
135
+ }
136
+ if (!hqEdge || typeof hqEdge.placeArtifact !== 'function') return null
137
+ let editorType = ''
138
+ try {
139
+ editorType = hqEdge.context?.getEditorType?.() ?? hqEdge.context?.getCurrent?.()?.editorType ?? ''
140
+ } catch {
141
+ return null
142
+ }
143
+ if (!editorType || !canPlaceArtifact(artifactType, editorType as EditorType)) return null
144
+ return {
145
+ editorType,
146
+ place: (request) => hqEdge.placeArtifact!(request),
147
+ }
148
+ }
149
+ }
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 {