@huaqiu/dsh-artifacts 0.3.6 → 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 +2 -1
- package/lib/index.mjs +2 -1
- package/lib/placement.d.mts +82 -0
- package/lib/placement.mjs +93 -0
- package/lib/service.d.mts +23 -0
- package/lib/service.mjs +23 -0
- package/package.json +5 -1
- package/src/index.ts +11 -0
- package/src/placement.ts +141 -0
- package/src/service.ts +34 -0
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.
|
|
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
|
|
package/src/placement.ts
ADDED
|
@@ -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 {
|