@finebi/finebi-cli 1.5.0 → 1.6.0
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/README.md +10 -0
- package/package.json +4 -3
- package/skills/finebi-cli-agent/SKILL.md +8 -4
- package/skills/finebi-cli-agent/references/command-workflows.md +7 -3
- package/skills/finebi-cli-agent/references/dashboard-data-result.md +191 -191
- package/skills/finebi-cli-agent/references/dashboard-dsl-api.md +8 -1018
- package/skills/finebi-cli-agent/references/dashboard-dsl-core.md +194 -0
- package/skills/finebi-cli-agent/references/dashboard-dsl-examples.md +378 -0
- package/skills/finebi-cli-agent/references/dashboard-dsl-items.md +456 -0
- package/skills/finebi-cli-agent/references/mapping-rules.md +14 -14
- package/skills/finebi-cli-agent/references/scenario-workflows.md +1 -1
- package/skills/finebi-cli-agent/references/web-app.md +13 -0
- package/skills/finebi-cli-agent/references/widget-dsl-api-examples.md +3 -4
- package/skills/finebi-cli-agent/references/widget-dsl-api.md +10 -1374
- package/skills/finebi-cli-agent/references/widget-dsl-contexts.md +142 -0
- package/skills/finebi-cli-agent/references/widget-dsl-core.md +374 -0
- package/skills/finebi-cli-agent/references/widget-dsl-encodings.md +457 -0
- package/skills/finebi-cli-agent/references/widget-dsl-styles.md +413 -0
- package/skills/finebi-cli-agent/scripts/finebi-cli/package.json +1 -1
- package/skills/finebi-cli-agent/scripts/finebi-cli/src/app.js +189 -0
- package/skills/finebi-cli-agent/scripts/finebi-cli/src/config.js +27 -27
- package/skills/finebi-cli-agent/scripts/finebi-cli/src/help.js +78 -55
- package/skills/finebi-cli-agent/scripts/finebi-cli/src/index.js +5 -0
- package/skills/finebi-cli-agent/scripts/finebi-cli/src/install-skill.js +89 -89
- package/skills/finebi-cli-agent/scripts/finebi-cli/src/profiles.js +158 -158
- package/skills/finebi-cli-agent/scripts/finebi-cli/src/skill-files.js +94 -94
- package/skills/finebi-cli-agent/scripts/finebi-cli/templates/web-app/AGENTS.md +27 -0
- package/skills/finebi-cli-agent/scripts/finebi-cli/templates/web-app/README.md +30 -0
- package/skills/finebi-cli-agent/scripts/finebi-cli/templates/web-app/docs/auth-and-deploy.md +17 -0
- package/skills/finebi-cli-agent/scripts/finebi-cli/templates/web-app/docs/data-contract.md +21 -0
- package/skills/finebi-cli-agent/scripts/finebi-cli/templates/web-app/docs/design.md +13 -0
- package/skills/finebi-cli-agent/scripts/finebi-cli/templates/web-app/docs/filters.md +89 -0
- package/skills/finebi-cli-agent/scripts/finebi-cli/templates/web-app/index.html +2 -0
- package/skills/finebi-cli-agent/scripts/finebi-cli/templates/web-app/package-lock.json +2191 -0
- package/skills/finebi-cli-agent/scripts/finebi-cli/templates/web-app/package.json +15 -0
- package/skills/finebi-cli-agent/scripts/finebi-cli/templates/web-app/public/app-config.json +11 -0
- package/skills/finebi-cli-agent/scripts/finebi-cli/templates/web-app/scripts/dev-proxy.mjs +56 -0
- package/skills/finebi-cli-agent/scripts/finebi-cli/templates/web-app/src/App.tsx +31 -0
- package/skills/finebi-cli-agent/scripts/finebi-cli/templates/web-app/src/components/Chart.tsx +31 -0
- package/skills/finebi-cli-agent/scripts/finebi-cli/templates/web-app/src/components/DataTable.tsx +14 -0
- package/skills/finebi-cli-agent/scripts/finebi-cli/templates/web-app/src/components/FilterBar.tsx +42 -0
- package/skills/finebi-cli-agent/scripts/finebi-cli/templates/web-app/src/components/Widget.tsx +28 -0
- package/skills/finebi-cli-agent/scripts/finebi-cli/templates/web-app/src/components/calendar.ts +14 -0
- package/skills/finebi-cli-agent/scripts/finebi-cli/templates/web-app/src/finebi/client.ts +37 -0
- package/skills/finebi-cli-agent/scripts/finebi-cli/templates/web-app/src/finebi/date.d.ts +1 -0
- package/skills/finebi-cli-agent/scripts/finebi-cli/templates/web-app/src/finebi/date.js +5 -0
- package/skills/finebi-cli-agent/scripts/finebi-cli/templates/web-app/src/finebi/filters.d.ts +4 -0
- package/skills/finebi-cli-agent/scripts/finebi-cli/templates/web-app/src/finebi/filters.js +106 -0
- package/skills/finebi-cli-agent/scripts/finebi-cli/templates/web-app/src/finebi/normalize.ts +77 -0
- package/skills/finebi-cli-agent/scripts/finebi-cli/templates/web-app/src/finebi/types.ts +18 -0
- package/skills/finebi-cli-agent/scripts/finebi-cli/templates/web-app/src/main.tsx +5 -0
- package/skills/finebi-cli-agent/scripts/finebi-cli/templates/web-app/src/services/view-data.ts +18 -0
- package/skills/finebi-cli-agent/scripts/finebi-cli/templates/web-app/src/styles.css +8 -0
- package/skills/finebi-cli-agent/scripts/finebi-cli/templates/web-app/tests/calendar.test.mjs +25 -0
- package/skills/finebi-cli-agent/scripts/finebi-cli/templates/web-app/tests/proxy.test.mjs +84 -0
- package/skills/finebi-cli-agent/scripts/finebi-cli/templates/web-app/tests/runtime.test.mjs +136 -0
- package/skills/finebi-cli-agent/scripts/finebi-cli/templates/web-app/tsconfig.json +4 -0
- package/skills/finebi-cli-agent/scripts/finebi-cli/templates/web-app/vite.config.ts +13 -0
- package/skills/finebi-cli-agent/scripts/finebi-cli-runner.mjs +4 -4
- package/src/app.js +189 -0
- package/src/config.js +27 -27
- package/src/help.js +78 -55
- package/src/index.js +5 -0
- package/src/install-skill.js +89 -89
- package/src/profiles.js +158 -158
- package/src/skill-files.js +94 -94
- package/templates/web-app/AGENTS.md +27 -0
- package/templates/web-app/README.md +30 -0
- package/templates/web-app/docs/auth-and-deploy.md +17 -0
- package/templates/web-app/docs/data-contract.md +21 -0
- package/templates/web-app/docs/design.md +13 -0
- package/templates/web-app/docs/filters.md +89 -0
- package/templates/web-app/index.html +2 -0
- package/templates/web-app/package-lock.json +2191 -0
- package/templates/web-app/package.json +15 -0
- package/templates/web-app/public/app-config.json +11 -0
- package/templates/web-app/scripts/dev-proxy.mjs +56 -0
- package/templates/web-app/src/App.tsx +31 -0
- package/templates/web-app/src/components/Chart.tsx +31 -0
- package/templates/web-app/src/components/DataTable.tsx +14 -0
- package/templates/web-app/src/components/FilterBar.tsx +42 -0
- package/templates/web-app/src/components/Widget.tsx +28 -0
- package/templates/web-app/src/components/calendar.ts +14 -0
- package/templates/web-app/src/finebi/client.ts +37 -0
- package/templates/web-app/src/finebi/date.d.ts +1 -0
- package/templates/web-app/src/finebi/date.js +5 -0
- package/templates/web-app/src/finebi/filters.d.ts +4 -0
- package/templates/web-app/src/finebi/filters.js +106 -0
- package/templates/web-app/src/finebi/normalize.ts +77 -0
- package/templates/web-app/src/finebi/types.ts +18 -0
- package/templates/web-app/src/main.tsx +5 -0
- package/templates/web-app/src/services/view-data.ts +18 -0
- package/templates/web-app/src/styles.css +8 -0
- package/templates/web-app/tests/calendar.test.mjs +25 -0
- package/templates/web-app/tests/proxy.test.mjs +84 -0
- package/templates/web-app/tests/runtime.test.mjs +136 -0
- package/templates/web-app/tsconfig.json +4 -0
- package/templates/web-app/vite.config.ts +13 -0
package/src/profiles.js
CHANGED
|
@@ -1,158 +1,158 @@
|
|
|
1
|
-
import {closeSync, existsSync, mkdirSync, openSync, rmSync} from "node:fs";
|
|
2
|
-
import {dirname, join} from "node:path";
|
|
3
|
-
|
|
4
|
-
import {normalizeConfig, readConfig, writeConfigAtomically} from "./config.js";
|
|
5
|
-
|
|
6
|
-
export class ProfileError extends Error {}
|
|
7
|
-
|
|
8
|
-
export function validateProfileName(name) {
|
|
9
|
-
if (typeof name !== "string" || !/^[a-zA-Z0-9][a-zA-Z0-9_-]{0,63}$/.test(name)) {
|
|
10
|
-
throw new ProfileError("Profile name must contain 1-64 letters, digits, underscores or hyphens and start with a letter or digit.");
|
|
11
|
-
}
|
|
12
|
-
return name;
|
|
13
|
-
}
|
|
14
|
-
|
|
15
|
-
export function readProfiles(path) {
|
|
16
|
-
const config = readConfig(path);
|
|
17
|
-
if (config.schemaVersion === 2) {
|
|
18
|
-
if (!config.profiles || typeof config.profiles !== "object" || Array.isArray(config.profiles)) {
|
|
19
|
-
throw new ProfileError("Invalid profiles configuration.");
|
|
20
|
-
}
|
|
21
|
-
return {multi: true, currentProfile: config.currentProfile ?? null, profiles: config.profiles};
|
|
22
|
-
}
|
|
23
|
-
// Preserve legacy session tokens when a single-environment file is migrated.
|
|
24
|
-
if (config.token && !config.auth?.token) {
|
|
25
|
-
config.auth = {...config.auth, token: config.token};
|
|
26
|
-
}
|
|
27
|
-
const present = existsSync(path);
|
|
28
|
-
return {multi: false, currentProfile: present ? "default" : null, profiles: present ? {default: config} : {}};
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
export function selectProfile(path, requested, {allowCreate = false, optional = false} = {}) {
|
|
32
|
-
const store = readProfiles(path);
|
|
33
|
-
const name = requested ?? store.currentProfile ?? (!store.multi ? "default" : null);
|
|
34
|
-
if (!name) {
|
|
35
|
-
if (optional) return null;
|
|
36
|
-
throw new ProfileError("No default environment is selected. Run finebi-cli auth use <name> or pass --profile <name>.");
|
|
37
|
-
}
|
|
38
|
-
validateProfileName(name);
|
|
39
|
-
const implicitInitial = !requested && !store.multi && !existsSync(path);
|
|
40
|
-
if (!Object.hasOwn(store.profiles, name) && !allowCreate && !implicitInitial && !optional) {
|
|
41
|
-
throw new ProfileError(`Environment '${name}' does not exist. Create it with finebi-cli auth login --profile ${name} --base-url <url>.`);
|
|
42
|
-
}
|
|
43
|
-
if (requested && !Object.hasOwn(store.profiles, name) && !allowCreate) {
|
|
44
|
-
throw new ProfileError(`Environment '${name}' does not exist.`);
|
|
45
|
-
}
|
|
46
|
-
return name;
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
export function readProfile(path, name) {
|
|
50
|
-
const store = readProfiles(path);
|
|
51
|
-
return normalizeConfig(name && Object.hasOwn(store.profiles, name) ? store.profiles[name] : {});
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
export function profileCapabilityPath(path, name) {
|
|
55
|
-
if (!name || name === "default") return path;
|
|
56
|
-
// Hex encoding preserves case-sensitive names on case-insensitive filesystems.
|
|
57
|
-
return join(dirname(path), `capabilities-profile-${Buffer.from(validateProfileName(name)).toString("hex")}.json`);
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
export function saveProfile(path, name, config, {makeCurrent = false, forceMulti = false, writer = writeConfigAtomically} = {}) {
|
|
61
|
-
validateProfileName(name);
|
|
62
|
-
return withProfileLock(path, () => {
|
|
63
|
-
const store = readProfiles(path);
|
|
64
|
-
if (!store.multi && name === "default" && !forceMulti) {
|
|
65
|
-
writer(path, config);
|
|
66
|
-
return;
|
|
67
|
-
}
|
|
68
|
-
const profiles = {...store.profiles, [name]: config};
|
|
69
|
-
writer(path, {schemaVersion: 2, currentProfile: makeCurrent ? name : store.currentProfile, profiles});
|
|
70
|
-
});
|
|
71
|
-
}
|
|
72
|
-
|
|
73
|
-
export function useProfile(path, name) {
|
|
74
|
-
validateProfileName(name);
|
|
75
|
-
return withProfileLock(path, () => {
|
|
76
|
-
const store = readProfiles(path);
|
|
77
|
-
if (!Object.hasOwn(store.profiles, name)) throw new ProfileError(`Environment '${name}' does not exist.`);
|
|
78
|
-
writeConfigAtomically(path, {schemaVersion: 2, currentProfile: name, profiles: store.profiles});
|
|
79
|
-
});
|
|
80
|
-
}
|
|
81
|
-
|
|
82
|
-
export function removeProfile(path, name) {
|
|
83
|
-
validateProfileName(name);
|
|
84
|
-
return withProfileLock(path, () => {
|
|
85
|
-
const store = readProfiles(path);
|
|
86
|
-
if (!Object.hasOwn(store.profiles, name)) {
|
|
87
|
-
if (!store.multi && name === "default") return;
|
|
88
|
-
throw new ProfileError(`Environment '${name}' does not exist.`);
|
|
89
|
-
}
|
|
90
|
-
if (!store.multi && name === "default") {
|
|
91
|
-
rmSync(path);
|
|
92
|
-
return;
|
|
93
|
-
}
|
|
94
|
-
const profiles = {...store.profiles};
|
|
95
|
-
delete profiles[name];
|
|
96
|
-
writeConfigAtomically(path, {schemaVersion: 2, currentProfile: store.currentProfile === name ? null : store.currentProfile, profiles});
|
|
97
|
-
});
|
|
98
|
-
}
|
|
99
|
-
|
|
100
|
-
export async function withProfileLoginLock(path, name, operation) {
|
|
101
|
-
validateProfileName(name);
|
|
102
|
-
mkdirSync(dirname(path), {recursive: true, mode: 0o700});
|
|
103
|
-
const lock = `${path}.login-${Buffer.from(name).toString("hex")}.lock`;
|
|
104
|
-
let fd;
|
|
105
|
-
try {
|
|
106
|
-
fd = openSync(lock, "wx", 0o600);
|
|
107
|
-
} catch (error) {
|
|
108
|
-
if (error.code === "EEXIST") throw new ProfileError(`An operation for environment '${name}' is already in progress. Retry after it finishes.`);
|
|
109
|
-
throw error;
|
|
110
|
-
}
|
|
111
|
-
try {
|
|
112
|
-
return await operation();
|
|
113
|
-
} finally {
|
|
114
|
-
closeSync(fd);
|
|
115
|
-
rmSync(lock, {force: true});
|
|
116
|
-
}
|
|
117
|
-
}
|
|
118
|
-
|
|
119
|
-
function withProfileLock(path, operation) {
|
|
120
|
-
mkdirSync(dirname(path), {recursive: true, mode: 0o700});
|
|
121
|
-
const lock = `${path}.lock`;
|
|
122
|
-
const deadline = Date.now() + 3000;
|
|
123
|
-
let fd;
|
|
124
|
-
while (fd === undefined) {
|
|
125
|
-
try {
|
|
126
|
-
fd = openSync(lock, "wx", 0o600);
|
|
127
|
-
} catch (error) {
|
|
128
|
-
if (error.code !== "EEXIST") throw error;
|
|
129
|
-
if (Date.now() >= deadline) throw new ProfileError("Configuration is locked by another process. Retry after it finishes.");
|
|
130
|
-
Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, 20);
|
|
131
|
-
}
|
|
132
|
-
}
|
|
133
|
-
try {
|
|
134
|
-
return operation();
|
|
135
|
-
} finally {
|
|
136
|
-
closeSync(fd);
|
|
137
|
-
rmSync(lock, {force: true});
|
|
138
|
-
}
|
|
139
|
-
}
|
|
140
|
-
|
|
141
|
-
export function parseConnectionArgs(argv) {
|
|
142
|
-
const args = [];
|
|
143
|
-
const flags = {};
|
|
144
|
-
const names = {"--profile": "profile", "--base-url": "baseUrl", "--token": "token"};
|
|
145
|
-
for (let index = 0; index < argv.length; index++) {
|
|
146
|
-
const [option, ...inline] = argv[index].split("=");
|
|
147
|
-
if (!Object.hasOwn(names, option)) {
|
|
148
|
-
args.push(argv[index]);
|
|
149
|
-
continue;
|
|
150
|
-
}
|
|
151
|
-
const value = inline.length ? inline.join("=") : argv[++index];
|
|
152
|
-
if (!value?.trim() || value.startsWith("--")) throw new ProfileError(`${option} requires a value.`);
|
|
153
|
-
const key = names[option];
|
|
154
|
-
if (Object.hasOwn(flags, key)) throw new ProfileError(`${option} must only be supplied once.`);
|
|
155
|
-
flags[key] = value.trim();
|
|
156
|
-
}
|
|
157
|
-
return {args, flags};
|
|
158
|
-
}
|
|
1
|
+
import {closeSync, existsSync, mkdirSync, openSync, rmSync} from "node:fs";
|
|
2
|
+
import {dirname, join} from "node:path";
|
|
3
|
+
|
|
4
|
+
import {normalizeConfig, readConfig, writeConfigAtomically} from "./config.js";
|
|
5
|
+
|
|
6
|
+
export class ProfileError extends Error {}
|
|
7
|
+
|
|
8
|
+
export function validateProfileName(name) {
|
|
9
|
+
if (typeof name !== "string" || !/^[a-zA-Z0-9][a-zA-Z0-9_-]{0,63}$/.test(name)) {
|
|
10
|
+
throw new ProfileError("Profile name must contain 1-64 letters, digits, underscores or hyphens and start with a letter or digit.");
|
|
11
|
+
}
|
|
12
|
+
return name;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export function readProfiles(path) {
|
|
16
|
+
const config = readConfig(path);
|
|
17
|
+
if (config.schemaVersion === 2) {
|
|
18
|
+
if (!config.profiles || typeof config.profiles !== "object" || Array.isArray(config.profiles)) {
|
|
19
|
+
throw new ProfileError("Invalid profiles configuration.");
|
|
20
|
+
}
|
|
21
|
+
return {multi: true, currentProfile: config.currentProfile ?? null, profiles: config.profiles};
|
|
22
|
+
}
|
|
23
|
+
// Preserve legacy session tokens when a single-environment file is migrated.
|
|
24
|
+
if (config.token && !config.auth?.token) {
|
|
25
|
+
config.auth = {...config.auth, token: config.token};
|
|
26
|
+
}
|
|
27
|
+
const present = existsSync(path);
|
|
28
|
+
return {multi: false, currentProfile: present ? "default" : null, profiles: present ? {default: config} : {}};
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export function selectProfile(path, requested, {allowCreate = false, optional = false} = {}) {
|
|
32
|
+
const store = readProfiles(path);
|
|
33
|
+
const name = requested ?? store.currentProfile ?? (!store.multi ? "default" : null);
|
|
34
|
+
if (!name) {
|
|
35
|
+
if (optional) return null;
|
|
36
|
+
throw new ProfileError("No default environment is selected. Run finebi-cli auth use <name> or pass --profile <name>.");
|
|
37
|
+
}
|
|
38
|
+
validateProfileName(name);
|
|
39
|
+
const implicitInitial = !requested && !store.multi && !existsSync(path);
|
|
40
|
+
if (!Object.hasOwn(store.profiles, name) && !allowCreate && !implicitInitial && !optional) {
|
|
41
|
+
throw new ProfileError(`Environment '${name}' does not exist. Create it with finebi-cli auth login --profile ${name} --base-url <url>.`);
|
|
42
|
+
}
|
|
43
|
+
if (requested && !Object.hasOwn(store.profiles, name) && !allowCreate) {
|
|
44
|
+
throw new ProfileError(`Environment '${name}' does not exist.`);
|
|
45
|
+
}
|
|
46
|
+
return name;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export function readProfile(path, name) {
|
|
50
|
+
const store = readProfiles(path);
|
|
51
|
+
return normalizeConfig(name && Object.hasOwn(store.profiles, name) ? store.profiles[name] : {});
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export function profileCapabilityPath(path, name) {
|
|
55
|
+
if (!name || name === "default") return path;
|
|
56
|
+
// Hex encoding preserves case-sensitive names on case-insensitive filesystems.
|
|
57
|
+
return join(dirname(path), `capabilities-profile-${Buffer.from(validateProfileName(name)).toString("hex")}.json`);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
export function saveProfile(path, name, config, {makeCurrent = false, forceMulti = false, writer = writeConfigAtomically} = {}) {
|
|
61
|
+
validateProfileName(name);
|
|
62
|
+
return withProfileLock(path, () => {
|
|
63
|
+
const store = readProfiles(path);
|
|
64
|
+
if (!store.multi && name === "default" && !forceMulti) {
|
|
65
|
+
writer(path, config);
|
|
66
|
+
return;
|
|
67
|
+
}
|
|
68
|
+
const profiles = {...store.profiles, [name]: config};
|
|
69
|
+
writer(path, {schemaVersion: 2, currentProfile: makeCurrent ? name : store.currentProfile, profiles});
|
|
70
|
+
});
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
export function useProfile(path, name) {
|
|
74
|
+
validateProfileName(name);
|
|
75
|
+
return withProfileLock(path, () => {
|
|
76
|
+
const store = readProfiles(path);
|
|
77
|
+
if (!Object.hasOwn(store.profiles, name)) throw new ProfileError(`Environment '${name}' does not exist.`);
|
|
78
|
+
writeConfigAtomically(path, {schemaVersion: 2, currentProfile: name, profiles: store.profiles});
|
|
79
|
+
});
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
export function removeProfile(path, name) {
|
|
83
|
+
validateProfileName(name);
|
|
84
|
+
return withProfileLock(path, () => {
|
|
85
|
+
const store = readProfiles(path);
|
|
86
|
+
if (!Object.hasOwn(store.profiles, name)) {
|
|
87
|
+
if (!store.multi && name === "default") return;
|
|
88
|
+
throw new ProfileError(`Environment '${name}' does not exist.`);
|
|
89
|
+
}
|
|
90
|
+
if (!store.multi && name === "default") {
|
|
91
|
+
rmSync(path);
|
|
92
|
+
return;
|
|
93
|
+
}
|
|
94
|
+
const profiles = {...store.profiles};
|
|
95
|
+
delete profiles[name];
|
|
96
|
+
writeConfigAtomically(path, {schemaVersion: 2, currentProfile: store.currentProfile === name ? null : store.currentProfile, profiles});
|
|
97
|
+
});
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
export async function withProfileLoginLock(path, name, operation) {
|
|
101
|
+
validateProfileName(name);
|
|
102
|
+
mkdirSync(dirname(path), {recursive: true, mode: 0o700});
|
|
103
|
+
const lock = `${path}.login-${Buffer.from(name).toString("hex")}.lock`;
|
|
104
|
+
let fd;
|
|
105
|
+
try {
|
|
106
|
+
fd = openSync(lock, "wx", 0o600);
|
|
107
|
+
} catch (error) {
|
|
108
|
+
if (error.code === "EEXIST") throw new ProfileError(`An operation for environment '${name}' is already in progress. Retry after it finishes.`);
|
|
109
|
+
throw error;
|
|
110
|
+
}
|
|
111
|
+
try {
|
|
112
|
+
return await operation();
|
|
113
|
+
} finally {
|
|
114
|
+
closeSync(fd);
|
|
115
|
+
rmSync(lock, {force: true});
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
function withProfileLock(path, operation) {
|
|
120
|
+
mkdirSync(dirname(path), {recursive: true, mode: 0o700});
|
|
121
|
+
const lock = `${path}.lock`;
|
|
122
|
+
const deadline = Date.now() + 3000;
|
|
123
|
+
let fd;
|
|
124
|
+
while (fd === undefined) {
|
|
125
|
+
try {
|
|
126
|
+
fd = openSync(lock, "wx", 0o600);
|
|
127
|
+
} catch (error) {
|
|
128
|
+
if (error.code !== "EEXIST") throw error;
|
|
129
|
+
if (Date.now() >= deadline) throw new ProfileError("Configuration is locked by another process. Retry after it finishes.");
|
|
130
|
+
Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, 20);
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
try {
|
|
134
|
+
return operation();
|
|
135
|
+
} finally {
|
|
136
|
+
closeSync(fd);
|
|
137
|
+
rmSync(lock, {force: true});
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
export function parseConnectionArgs(argv) {
|
|
142
|
+
const args = [];
|
|
143
|
+
const flags = {};
|
|
144
|
+
const names = {"--profile": "profile", "--base-url": "baseUrl", "--token": "token"};
|
|
145
|
+
for (let index = 0; index < argv.length; index++) {
|
|
146
|
+
const [option, ...inline] = argv[index].split("=");
|
|
147
|
+
if (!Object.hasOwn(names, option)) {
|
|
148
|
+
args.push(argv[index]);
|
|
149
|
+
continue;
|
|
150
|
+
}
|
|
151
|
+
const value = inline.length ? inline.join("=") : argv[++index];
|
|
152
|
+
if (!value?.trim() || value.startsWith("--")) throw new ProfileError(`${option} requires a value.`);
|
|
153
|
+
const key = names[option];
|
|
154
|
+
if (Object.hasOwn(flags, key)) throw new ProfileError(`${option} must only be supplied once.`);
|
|
155
|
+
flags[key] = value.trim();
|
|
156
|
+
}
|
|
157
|
+
return {args, flags};
|
|
158
|
+
}
|
package/src/skill-files.js
CHANGED
|
@@ -1,94 +1,94 @@
|
|
|
1
|
-
import {createHash, randomUUID} from "node:crypto";
|
|
2
|
-
import {existsSync} from "node:fs";
|
|
3
|
-
import {cp, mkdir, lstat, mkdtemp, readFile, readdir, rename, rm} from "node:fs/promises";
|
|
4
|
-
import {homedir} from "node:os";
|
|
5
|
-
import {join, dirname, resolve} from "node:path";
|
|
6
|
-
|
|
7
|
-
export function agentCatalog({home = homedir(), env = process.env} = {}) {
|
|
8
|
-
const config = env.XDG_CONFIG_HOME || join(home, ".config");
|
|
9
|
-
return [
|
|
10
|
-
["claude-code", "Claude Code", env.CLAUDE_CONFIG_DIR || join(home, ".claude")],
|
|
11
|
-
["codex", "Codex", env.CODEX_HOME || join(home, ".codex")],
|
|
12
|
-
["cursor", "Cursor", join(home, ".cursor")],
|
|
13
|
-
["gemini-cli", "Gemini CLI", join(home, ".gemini")],
|
|
14
|
-
["github-copilot", "GitHub Copilot", join(home, ".copilot")],
|
|
15
|
-
["opencode", "OpenCode", join(config, "opencode")],
|
|
16
|
-
["cline", "Cline", join(home, ".agents")],
|
|
17
|
-
["windsurf", "Windsurf", join(home, ".codeium", "windsurf")],
|
|
18
|
-
["trae", "Trae", join(home, ".trae")],
|
|
19
|
-
["trae-cn", "Trae CN", join(home, ".trae-cn")],
|
|
20
|
-
["openclaw", "OpenClaw", [".openclaw", ".clawdbot", ".moltbot"].map(p => join(home, p)).find(existsSync) || join(home, ".openclaw")],
|
|
21
|
-
["roo", "Roo Code", join(home, ".roo")],
|
|
22
|
-
["qwen-code", "Qwen Code", join(home, ".qwen")],
|
|
23
|
-
].map(([id, name, root]) => ({id, name, directory: join(root, "skills"), detected: existsSync(root)}));
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
export async function hashTree(root, prefix = "") {
|
|
27
|
-
const files = {};
|
|
28
|
-
for (const entry of (await readdir(join(root, prefix), {withFileTypes: true})).sort((a, b) => a.name.localeCompare(b.name))) {
|
|
29
|
-
const name = prefix ? `${prefix}/${entry.name}` : entry.name;
|
|
30
|
-
if (entry.isSymbolicLink()) throw new Error(`Skill contains a symbolic link: ${name}`);
|
|
31
|
-
if (entry.isDirectory()) Object.assign(files, await hashTree(root, name));
|
|
32
|
-
else files[name] = createHash("sha256").update(await readFile(join(root, name))).digest("hex");
|
|
33
|
-
}
|
|
34
|
-
return files;
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
function sameHashes(left, right) {
|
|
38
|
-
return left && right && Object.keys(left).length === Object.keys(right).length
|
|
39
|
-
&& Object.keys(left).every(key => left[key] === right[key]);
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
export async function installSkills({source, home, env, agents, replace = false, previous = []}) {
|
|
43
|
-
const catalog = agentCatalog({home, env});
|
|
44
|
-
const selected = agents.map(id => {
|
|
45
|
-
const agent = catalog.find(item => item.id === id);
|
|
46
|
-
if (!agent) throw new Error(`Unknown Agent: ${id}`);
|
|
47
|
-
return agent;
|
|
48
|
-
});
|
|
49
|
-
const sourceHashes = await hashTree(source);
|
|
50
|
-
const results = [];
|
|
51
|
-
for (const agent of selected) {
|
|
52
|
-
const target = join(agent.directory, "finebi-cli-agent");
|
|
53
|
-
let backup;
|
|
54
|
-
let candidate;
|
|
55
|
-
try {
|
|
56
|
-
const stat = await lstat(target).catch(error => { if (error.code !== "ENOENT") throw error; });
|
|
57
|
-
if (stat) {
|
|
58
|
-
if (stat.isSymbolicLink()) throw new Error("Skill target is a symbolic link; refusing to replace it.");
|
|
59
|
-
const actual = await hashTree(target);
|
|
60
|
-
if (sameHashes(sourceHashes, actual)) {
|
|
61
|
-
results.push({agent: agent.id, path: target, status: "unchanged", hashes: sourceHashes});
|
|
62
|
-
continue;
|
|
63
|
-
}
|
|
64
|
-
const recorded = previous.find(item => item.agent === agent.id && item.path === target)?.hashes;
|
|
65
|
-
if (!replace && !sameHashes(recorded, actual)) {
|
|
66
|
-
results.push({agent: agent.id, path: target, status: "kept"});
|
|
67
|
-
continue;
|
|
68
|
-
}
|
|
69
|
-
backup = join(home, ".finebi-cli", "skill-backups", `${agent.id}-${randomUUID()}`);
|
|
70
|
-
await mkdir(dirname(backup), {recursive: true});
|
|
71
|
-
await cp(target, backup, {recursive: true, errorOnExist: true, force: false});
|
|
72
|
-
if (!sameHashes(actual, await hashTree(backup))) throw new Error("Skill backup integrity check failed.");
|
|
73
|
-
}
|
|
74
|
-
await mkdir(agent.directory, {recursive: true});
|
|
75
|
-
candidate = await mkdtemp(join(agent.directory, ".finebi-cli-stage-"));
|
|
76
|
-
await cp(source, candidate, {recursive: true});
|
|
77
|
-
if (!sameHashes(sourceHashes, await hashTree(candidate))) throw new Error("Skill copy integrity check failed.");
|
|
78
|
-
const old = `${candidate}-old`;
|
|
79
|
-
if (stat) await rename(target, old);
|
|
80
|
-
try { await rename(candidate, target); }
|
|
81
|
-
catch (error) { if (stat) await rename(old, target); throw error; }
|
|
82
|
-
candidate = undefined;
|
|
83
|
-
// Delete only the relocated, backed-up directory inside this Agent's skills directory.
|
|
84
|
-
if (stat && backup && dirname(resolve(old)) === resolve(agent.directory)) await rm(old, {recursive: true});
|
|
85
|
-
if (!sameHashes(sourceHashes, await hashTree(target))) throw new Error("Installed Skill integrity check failed.");
|
|
86
|
-
results.push({agent: agent.id, path: target, status: "installed", hashes: sourceHashes, ...(backup ? {backup} : {})});
|
|
87
|
-
} catch (error) {
|
|
88
|
-
results.push({agent: agent.id, path: target, status: "failed", error: error.message, ...(backup ? {backup} : {})});
|
|
89
|
-
} finally {
|
|
90
|
-
if (candidate && dirname(resolve(candidate)) === resolve(agent.directory)) await rm(candidate, {recursive: true, force: true});
|
|
91
|
-
}
|
|
92
|
-
}
|
|
93
|
-
return results;
|
|
94
|
-
}
|
|
1
|
+
import {createHash, randomUUID} from "node:crypto";
|
|
2
|
+
import {existsSync} from "node:fs";
|
|
3
|
+
import {cp, mkdir, lstat, mkdtemp, readFile, readdir, rename, rm} from "node:fs/promises";
|
|
4
|
+
import {homedir} from "node:os";
|
|
5
|
+
import {join, dirname, resolve} from "node:path";
|
|
6
|
+
|
|
7
|
+
export function agentCatalog({home = homedir(), env = process.env} = {}) {
|
|
8
|
+
const config = env.XDG_CONFIG_HOME || join(home, ".config");
|
|
9
|
+
return [
|
|
10
|
+
["claude-code", "Claude Code", env.CLAUDE_CONFIG_DIR || join(home, ".claude")],
|
|
11
|
+
["codex", "Codex", env.CODEX_HOME || join(home, ".codex")],
|
|
12
|
+
["cursor", "Cursor", join(home, ".cursor")],
|
|
13
|
+
["gemini-cli", "Gemini CLI", join(home, ".gemini")],
|
|
14
|
+
["github-copilot", "GitHub Copilot", join(home, ".copilot")],
|
|
15
|
+
["opencode", "OpenCode", join(config, "opencode")],
|
|
16
|
+
["cline", "Cline", join(home, ".agents")],
|
|
17
|
+
["windsurf", "Windsurf", join(home, ".codeium", "windsurf")],
|
|
18
|
+
["trae", "Trae", join(home, ".trae")],
|
|
19
|
+
["trae-cn", "Trae CN", join(home, ".trae-cn")],
|
|
20
|
+
["openclaw", "OpenClaw", [".openclaw", ".clawdbot", ".moltbot"].map(p => join(home, p)).find(existsSync) || join(home, ".openclaw")],
|
|
21
|
+
["roo", "Roo Code", join(home, ".roo")],
|
|
22
|
+
["qwen-code", "Qwen Code", join(home, ".qwen")],
|
|
23
|
+
].map(([id, name, root]) => ({id, name, directory: join(root, "skills"), detected: existsSync(root)}));
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export async function hashTree(root, prefix = "") {
|
|
27
|
+
const files = {};
|
|
28
|
+
for (const entry of (await readdir(join(root, prefix), {withFileTypes: true})).sort((a, b) => a.name.localeCompare(b.name))) {
|
|
29
|
+
const name = prefix ? `${prefix}/${entry.name}` : entry.name;
|
|
30
|
+
if (entry.isSymbolicLink()) throw new Error(`Skill contains a symbolic link: ${name}`);
|
|
31
|
+
if (entry.isDirectory()) Object.assign(files, await hashTree(root, name));
|
|
32
|
+
else files[name] = createHash("sha256").update(await readFile(join(root, name))).digest("hex");
|
|
33
|
+
}
|
|
34
|
+
return files;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function sameHashes(left, right) {
|
|
38
|
+
return left && right && Object.keys(left).length === Object.keys(right).length
|
|
39
|
+
&& Object.keys(left).every(key => left[key] === right[key]);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export async function installSkills({source, home, env, agents, replace = false, previous = []}) {
|
|
43
|
+
const catalog = agentCatalog({home, env});
|
|
44
|
+
const selected = agents.map(id => {
|
|
45
|
+
const agent = catalog.find(item => item.id === id);
|
|
46
|
+
if (!agent) throw new Error(`Unknown Agent: ${id}`);
|
|
47
|
+
return agent;
|
|
48
|
+
});
|
|
49
|
+
const sourceHashes = await hashTree(source);
|
|
50
|
+
const results = [];
|
|
51
|
+
for (const agent of selected) {
|
|
52
|
+
const target = join(agent.directory, "finebi-cli-agent");
|
|
53
|
+
let backup;
|
|
54
|
+
let candidate;
|
|
55
|
+
try {
|
|
56
|
+
const stat = await lstat(target).catch(error => { if (error.code !== "ENOENT") throw error; });
|
|
57
|
+
if (stat) {
|
|
58
|
+
if (stat.isSymbolicLink()) throw new Error("Skill target is a symbolic link; refusing to replace it.");
|
|
59
|
+
const actual = await hashTree(target);
|
|
60
|
+
if (sameHashes(sourceHashes, actual)) {
|
|
61
|
+
results.push({agent: agent.id, path: target, status: "unchanged", hashes: sourceHashes});
|
|
62
|
+
continue;
|
|
63
|
+
}
|
|
64
|
+
const recorded = previous.find(item => item.agent === agent.id && item.path === target)?.hashes;
|
|
65
|
+
if (!replace && !sameHashes(recorded, actual)) {
|
|
66
|
+
results.push({agent: agent.id, path: target, status: "kept"});
|
|
67
|
+
continue;
|
|
68
|
+
}
|
|
69
|
+
backup = join(home, ".finebi-cli", "skill-backups", `${agent.id}-${randomUUID()}`);
|
|
70
|
+
await mkdir(dirname(backup), {recursive: true});
|
|
71
|
+
await cp(target, backup, {recursive: true, errorOnExist: true, force: false});
|
|
72
|
+
if (!sameHashes(actual, await hashTree(backup))) throw new Error("Skill backup integrity check failed.");
|
|
73
|
+
}
|
|
74
|
+
await mkdir(agent.directory, {recursive: true});
|
|
75
|
+
candidate = await mkdtemp(join(agent.directory, ".finebi-cli-stage-"));
|
|
76
|
+
await cp(source, candidate, {recursive: true});
|
|
77
|
+
if (!sameHashes(sourceHashes, await hashTree(candidate))) throw new Error("Skill copy integrity check failed.");
|
|
78
|
+
const old = `${candidate}-old`;
|
|
79
|
+
if (stat) await rename(target, old);
|
|
80
|
+
try { await rename(candidate, target); }
|
|
81
|
+
catch (error) { if (stat) await rename(old, target); throw error; }
|
|
82
|
+
candidate = undefined;
|
|
83
|
+
// Delete only the relocated, backed-up directory inside this Agent's skills directory.
|
|
84
|
+
if (stat && backup && dirname(resolve(old)) === resolve(agent.directory)) await rm(old, {recursive: true});
|
|
85
|
+
if (!sameHashes(sourceHashes, await hashTree(target))) throw new Error("Installed Skill integrity check failed.");
|
|
86
|
+
results.push({agent: agent.id, path: target, status: "installed", hashes: sourceHashes, ...(backup ? {backup} : {})});
|
|
87
|
+
} catch (error) {
|
|
88
|
+
results.push({agent: agent.id, path: target, status: "failed", error: error.message, ...(backup ? {backup} : {})});
|
|
89
|
+
} finally {
|
|
90
|
+
if (candidate && dirname(resolve(candidate)) === resolve(agent.directory)) await rm(candidate, {recursive: true, force: true});
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
return results;
|
|
94
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
# FineBI 动态页面开发
|
|
2
|
+
|
|
3
|
+
## 开发顺序
|
|
4
|
+
|
|
5
|
+
1. 用 FineBI CLI 搜索并读取 dashboard info/data,确认看板实例 entryId、queryFields.dId、真实控件 filterName 和可访问来源。
|
|
6
|
+
2. 阅读 [数据契约](docs/data-contract.md)、[筛选](docs/filters.md)、[认证部署](docs/auth-and-deploy.md),再修改 public/app-config.json。
|
|
7
|
+
3. 从 overview 或 workbench 布局开始,按页面需求选择图表库和UI组件库;默认ECharts和Ant Design筛选控件均可替换,约定见[页面骨架](docs/design.md)。业务转换放 src/services,页面放 src/pages 或 App.tsx。通用请求与原生解析集中在 src/finebi,避免页面散落 fetch 或重复协议实现。
|
|
8
|
+
4. 使用真实接口开发;样本仅用于 tests,禁止将开发阶段查询数据写入正式页面、配置或回退数据。
|
|
9
|
+
5. npm test、npm run build,再通过浏览器核对请求、字段、图表、刷新和权限状态。构建成功不等于认证或取数成功。
|
|
10
|
+
|
|
11
|
+
## 数据规则
|
|
12
|
+
|
|
13
|
+
- 先检查 normalization、warnings、range,再选择唯一 dataset;多模型需显式 datasetId。
|
|
14
|
+
- display 用于显示;计算仅使用 AVAILABLE 的 value,保留 DECIMAL_STRING 精度。禁止从百分比/千分位/单位文本恢复数值。受限值、未知值不能补零。
|
|
15
|
+
- 不按名称猜字段、不累加 UNKNOWN 行、不从首页估算全量 KPI。KPI 示例仅消费单行结果。
|
|
16
|
+
- 控件筛选通过真实名称生成 refer;无控件时不伪造服务端过滤。搜索和排序的范围必须在页面说明。
|
|
17
|
+
- 筛选绑定先核对源控件类型,配置/状态/请求统一通过 src/finebi/filters.js 校验。完整支持矩阵、逗号和 null 保留字、恢复默认与筛选空值语义见 docs/filters.md;不以更换 type 绕过服务端能力限制。
|
|
18
|
+
- pageLimit 是首页大小,不是分页。不得循环放大请求以绕过 maxRows;hasNext=true 必须保留警告,不能宣称全量。
|
|
19
|
+
|
|
20
|
+
## 认证与交付
|
|
21
|
+
|
|
22
|
+
- 开发凭证只存在服务端环境 FINEBI_DEV_* 或内存;禁止 VITE_* 凭证、前端 localStorage token、提交 .env 和认证日志。
|
|
23
|
+
- 生产应用使用同源当前用户 Cookie。开发代理不属于构建产物;不要把它部署为共享多用户服务。
|
|
24
|
+
- v1 不支持目录发布来源、交叉表、服务端续页、全局排序或静态托管发布;遇到需求先说明缺口。
|
|
25
|
+
- 图表按生命周期销毁;异步请求取消且旧响应不得覆盖新条件。失败状态不得展示过期数据充当最新结果。
|
|
26
|
+
|
|
27
|
+
项目创建不会安装依赖、启动服务、初始化 Git 或发布,按用户授权范围执行后续步骤。
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
# FineBI 动态数据应用
|
|
2
|
+
|
|
3
|
+
这是运行时调用 FineBI 的静态应用。开发者通过 CLI 发现资源后配置绑定;生产查询使用访问者自己的登录态。
|
|
4
|
+
|
|
5
|
+
## 开始
|
|
6
|
+
|
|
7
|
+
1. 安装依赖:`npm ci`。
|
|
8
|
+
2. 填写 `public/app-config.json`。`dashboardId` 来自目标看板;`widgets[].entryId` 使用板内实例 ID;图表 `categoryFieldId/valueFieldId` 使用本次 `dashboard info` 的结果字段 dId。相同实例可以有不同展示,但 KPI 必须对应单行查询结果。
|
|
9
|
+
3. 在当前进程设置 `FINEBI_DEV_BASE_URL`(完整 decision 地址)和 `FINEBI_DEV_USERNAME/FINEBI_DEV_PASSWORD`,或 `FINEBI_DEV_TOKEN`。不要把值写入 VITE_* 或 app-config。
|
|
10
|
+
4. `npm run dev`,访问 `http://localhost:4173`。开发代理仅监听本机,自动以开发身份登录,凭证不返回浏览器。
|
|
11
|
+
5. `npm test`、`npm run build`。`npm run preview` 只展示 dist,不启用开发身份代理;真实生产取数需要同源 FineBI 路径。
|
|
12
|
+
|
|
13
|
+
## 配置
|
|
14
|
+
|
|
15
|
+
`layout` 为 `overview`(顶部概览)或 `workbench`(侧栏工作台)。`maxRows` 默认 1000,上限 10000;`pageSize` 是本地展示页大小,1–100。
|
|
16
|
+
|
|
17
|
+
```json
|
|
18
|
+
{
|
|
19
|
+
"schemaVersion":"1", "title":"销售分析", "basePath":"/webroot/decision",
|
|
20
|
+
"layout":"overview", "dashboardId":"真实看板ID", "maxRows":1000, "pageSize":8,
|
|
21
|
+
"widgets":[
|
|
22
|
+
{"entryId":"真实实例ID", "name":"销售趋势", "type":"line", "categoryFieldId":"日期结果字段ID", "valueFieldId":"销售结果字段ID"}
|
|
23
|
+
],
|
|
24
|
+
"filters":[]
|
|
25
|
+
}
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
默认组件 type 支持 table、kpi、bar、line、pie;多模型时指定 datasetId。配置不包含数据或任何凭证。协议和限制见 [数据契约](docs/data-contract.md)、[筛选与联动](docs/filters.md)、[认证与部署](docs/auth-and-deploy.md)。布局、图表库和UI组件库的选型与替换约定见 [页面骨架](docs/design.md)。
|
|
29
|
+
|
|
30
|
+
筛选先编辑、再点击“应用筛选”;“恢复全部默认”重新查询看板保存条件。模板支持文本、单选/多选、数值选择和日期类控件,完整类型、空值语义与配置示例以[筛选契约](docs/filters.md)为准;`app inspect` 仅检查本地配置,绑定后仍需验证真实条件生效。
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
# 认证与部署
|
|
2
|
+
|
|
3
|
+
## 本地开发
|
|
4
|
+
|
|
5
|
+
Vite开发插件读取服务端环境 `FINEBI_DEV_BASE_URL`、`FINEBI_DEV_USERNAME/FINEBI_DEV_PASSWORD`,或已有短期 `FINEBI_DEV_TOKEN`。BASE_URL必须是目标完整decision地址。支持由本地.env.local提供,禁止提交文件;不要使用VITE_前缀。
|
|
6
|
+
|
|
7
|
+
代理在内存保存开发会话,只接受本机同源、带XMLHttpRequest标记的POST;白名单限制为配置看板及组件查询,不代理登录管理或任意接口。浏览器无需持有开发账号凭证。失效返回AUTH_REQUIRED;密码方式下一次显式重试重新登录,token方式需更新token并重启开发服务。不要向他人开放开发服务端口。
|
|
8
|
+
|
|
9
|
+
筛选仅允许配置中的控件名和单值数组:下拉值必须属于 options,date 值必须是有效的 YYYY-MM-DD 日历日期,不使用固定日期白名单。空日期通过省略参数恢复保存条件,代理拒绝空字符串和多值日期数组。
|
|
10
|
+
|
|
11
|
+
## 生产
|
|
12
|
+
|
|
13
|
+
`npm run build` 输出dist,开发代理不会进入产物。将静态资源托管在与FineBI相同源的应用子路径,basePath配置为实际decision路径。用户先通过FineBI登录,查询携带当前Cookie,服务端按当前身份判断资源、数据和字段权限。
|
|
14
|
+
|
|
15
|
+
登录失效、HTTP403、查询失败分别显示状态,用户登录后点击重试。生产包不保存开发token、不调用CLI、不自动以开发者账号重登。`npm run preview`不注入身份,只用于产物展示;本版不提供部署上传/发布命令、跨域认证、嵌入SSO或目录发布来源查询。
|
|
16
|
+
|
|
17
|
+
交付应检查dist无凭证,核对真实查询网络与页面;开发身份验证通过不能证明所有生产用户有权访问,发布目录权限需另行验证。
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
# 数据契约与适配范围
|
|
2
|
+
|
|
3
|
+
## 输入与版本
|
|
4
|
+
|
|
5
|
+
浏览器调用 `/v5/api/dashboard/report/{dashboardId}/widgets/query`,传板内实例 widgetIds、includeWidgetBean=true、pageLimit、refer。原生结果仅在 src/finebi/normalize.ts 解析。输出 schemaVersion=1,与 CLI 标准化结果采用相同的字段、值状态与范围语义;v1 浏览器只覆盖 DETAIL 和 CHART,不等于 Java 适配器的全部覆盖。
|
|
6
|
+
|
|
7
|
+
## 消费顺序
|
|
8
|
+
|
|
9
|
+
检查 normalization(COMPLETE/PARTIAL/UNSUPPORTED)→ warnings → 选唯一 dataset → 按 columns 解释 rows[].cells → 按 range 说明范围。COMPLETE 只表示本次结构转换完整,不表示全集。多模型需指定 datasetId;UNKNOWN 行不得自动求和。
|
|
10
|
+
|
|
11
|
+
columns 含 key、fieldId、name、type、encoding。key 仅在本次dataset内唯一;fieldId 是结果字段ID,不能当底层数据集字段ID。绑定使用fieldId,不使用可能重复的name。
|
|
12
|
+
|
|
13
|
+
cells 含 value、display、state、reason:AVAILABLE 可用,NULL 确认空值,UNAVAILABLE 无法可靠解释,RESTRICTED 权限受限。表格优先显示 display,受限单元格统一显示“受限”。计算不能从 display 反解析数字;DECIMAL_STRING 保留上游值,JS Number 仅用于图形坐标且不能用于高精度金额计算。DATE 的 EPOCH_MILLIS_STRING 保留毫秒字符串,不擅自改变时区。日期元数据支持原生类型3、13、15、20;未知类型保守返回不可用。
|
|
14
|
+
|
|
15
|
+
浏览器适配器不能获知服务端 cellCharLimit,字符串不能据此承诺未经截断的数据库原文。图表可见数值不代表底层明细;数值型字符串不会被猜成图表指标。交叉表/分组表及缺少内联模型的图形明确为不支持;多模型、computedState 和缺列都会保留警告。
|
|
16
|
+
|
|
17
|
+
## 范围
|
|
18
|
+
|
|
19
|
+
明细表 range.total 采用有效原生row;PAGE是本次首页,pageLimit不提供续页。hasNext=true 原样保留并产生 NEXT_PAGE_UNSUPPORTED;即使返回数等于总数,也不覆盖这一状态。当前仅提供已加载结果的搜索、排序和分页,页面显示该范围。图表UNPAGED也不等于底层明细全量,可能存在Top N或聚合。
|
|
20
|
+
|
|
21
|
+
服务端返回空数组与缺失组件不同:前者可显示空数据,后者是 INVALID_RESPONSE。受限/失败/不支持结果不回退静态样本。
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
# 页面骨架
|
|
2
|
+
|
|
3
|
+
图表库和UI组件库不限定,按页面的展示、交互和设计需求选择。模板默认使用ECharts绘图,筛选栏默认使用Ant Design和date-fns;开发时可替换为D3等图表实现,或选用合适的UI组件库,也可使用原生控件。更换库需要调整对应展示组件、依赖和样式,不是app-config中的自动切换选项。
|
|
4
|
+
|
|
5
|
+
替换展示实现时继续复用src/finebi的取数与认证逻辑,遵循[数据契约](data-contract.md)和[筛选契约](filters.md),保留值状态、数据范围提示、空/错误状态及请求取消处理;按所选库的生命周期释放图表、监听器和其他资源。
|
|
6
|
+
|
|
7
|
+
默认FilterBar提供可搜索单选/多选、日期及年月区间日历、输入框、空值勾选、重置和应用按钮;其他日期类输入保留对应格式。日历与选择器采用中文界面,筛选卡片展示待应用状态,小屏幕自动换行,区间日历可滚动。主题在FilterBar的ConfigProvider中配置,布局在src/styles.css中调整。搜索仅过滤配置的候选项,不触发远端候选查询。
|
|
8
|
+
|
|
9
|
+
`layout=overview` 使用顶部标题/刷新、筛选栏、双列图表和通栏表格,适合单页分析。
|
|
10
|
+
|
|
11
|
+
`layout=workbench` 增加侧栏应用导航,适合后续扩展多页面。两者共用src/styles.css的颜色、间距和面板规则;小屏幕收为单列。通过修改应用页面与业务组件扩展,不复制请求/认证/解析代码。
|
|
12
|
+
|
|
13
|
+
默认Chart组件基于ECharts提供柱状、折线和饼图的基本映射、resize和dispose;Widget提供KPI和错误边界,DataTable提供范围明确的本地搜索/分页。Agent按业务绑定categoryFieldId/valueFieldId,不以返回形状猜图表类型。复杂图形需新增明确的适配与测试。
|