# Conflicts:
#	frontend-web/src/open-canvas/components/ai-writing/AIWritingForm.tsx
This commit is contained in:
董宏凯 committed 2026-07-11 19:01:10 +08:00
commit 27e6b0b23e
718 files changed
+1168 -142636

No files matched your search

+1 -1
View File
@@ -42,7 +42,7 @@ VITE_AIPI_BACKENP_URL=http://47.88.25.99:8000
# VITE_AIPI_FRONTEND_URL=https://knowledgetoolserver.b.uat.4.cn
VITE_NOTEBOOK_API_BASE_URL=http://47.88.25.99:8000
VITE_AIPI_FRONTEND_URL=http://47.88.25.99:3000
VITE_AIPI_CONSUMER_URL=https://ch1.b.uat.4.cn/consumer
VITE_AIPI_CONSUMER_URL=http://10.19.200.5:8002/consumer
# A 系统跳转地址(人物/组织/事件等列表项打开链接)
VITE_AIPI_PRESENT_URL=http://present.a.uat.4.cn/present/domain
# B 系统知识库 iframe 根地址
+3 -2
View File
@@ -15,8 +15,9 @@
</script>
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<link rel="icon" type="image/svg+xml" href="/favicon.svg" />
<title>CM 助手</title>
</head>
<title>CM 助手</title>
<script src="/runtime-config.js"></script>
</head>
<body>
<div id="root"></div>
<script type="module" src="/src/main.tsx"></script>
+4 -1
View File
@@ -10,7 +10,9 @@
"build:test": "node --max-old-space-size=8192 ./node_modules/vite/bin/vite.js build --mode test",
"build:network": "node --max-old-space-size=8192 ./node_modules/vite/bin/vite.js build --mode network",
"preview": "vite preview",
"typecheck": "tsc --noEmit"
"typecheck": "tsc --noEmit",
"check:chrome109-css": "node scripts/check-chrome109-css.mjs",
"check:legacy-chrome-css": "node scripts/check-chrome109-css.mjs"
},
"dependencies": {
"@codemirror/lang-css": "^6.3.1",
@@ -110,6 +112,7 @@
"@types/react": "^19.0.0",
"@types/react-dom": "^19.0.0",
"@vitejs/plugin-react": "^5.0.4",
"lightningcss": "^1.30.2",
"postcss": "^8.5.3",
"tailwindcss": "^4.0.15",
"tw-animate-css": "^1.4.0",
+11 -5
View File
@@ -1,5 +1,11 @@
export default {
plugins: {
"@tailwindcss/postcss": {},
},
};
import tailwindcss from "@tailwindcss/postcss";
import legacyColorFallback from "./scripts/postcss-legacy-color-fallback.mjs";
import lightningCssLegacyChrome from "./scripts/postcss-lightningcss-chrome109.mjs";
export default {
plugins: [
tailwindcss(),
legacyColorFallback(),
lightningCssLegacyChrome(),
],
};
+96
View File
@@ -0,0 +1,96 @@
// 前端运行时配置文件
//
// 说明:
// 1. 本文件会在打包时原样复制到 dist/runtime-config.js。
// 2. 应用启动时会优先读取 window.__CM_RUNTIME_CONFIG__ 里的配置。
// 3. 部署后如果需要切换接口地址,直接修改 dist/runtime-config.js 即可,不需要重新 pnpm build。
// 4. 这里的默认值来自 .env.network;如某个环境不用某项,可以留空字符串。
// 5. 布尔开关也用字符串形式填写:"true" / "false"。
window.__CM_RUNTIME_CONFIG__ = {
// DeerFlow Gateway 地址:前端自有后端接口,例如 /api/agents、/api/auth、/api/threads 等。
// VITE_BACKEND_BASE_URL: "http://10.19.200.5:2026",
// LangGraph 流式对话接口地址:普通问答、智能体对话等流式能力使用。
// VITE_LANGGRAPH_BASE_URL: "http://10.19.200.5:2026/api",
// Canvas/AI 写作独立 LangGraph 地址:为空时前端会回退使用 VITE_LANGGRAPH_BASE_URL。
VITE_CANVAS_LANGGRAPH_BASE_URL: "",
// 轻应用「新建笔记」系列接口地址。
VITE_NOTE_API_BASE_URL: "http://47.88.25.99:8000",
// CMZS 后端接口:任务管理、任务分析、三情分析、行动分析等业务接口。
VITE_API_BASE_URL: "https://ch1.b.uat.4.cn/zncm",
// 佳来模型对话接口。
VITE_AIPI_CHAT_URL: "https://ch1.b.uat.4.cn/gpt",
// 多轮检索/生成接口。
VITE_AIPI_GENERATE_CHAT_URL: "https://ch1.b.uat.4.cn/jialai_b1_chat",
// 任务问答接口。
VITE_AIPI_TASK_CHAT_URL: "https://ch1.b.uat.4.cn/qq_b1_chat",
// B 系统知识库前端地址:用于打开知识详情、原文详情等。
VITE_AIPI_KNOWLEDGE_URL: "https://chlib.b.uat.4.cn/knowlibrary",
// B1 COH 系统地址。
VITE_AIPI_B1COH_URL: "https://ch1.b.uat.4.cn/web",
// B2 行为体/网络资源系统地址。
VITE_AIPI_B2ACTOR_URL: "https://ty.b.uat.4.cn",
// B2 网络 XWT iframe 路径。
VITE_AIPI_B2ACTOR_XWT_PATH: "/fromB/toXWT?isFrame=1",
// 知识库 iframe 地址换取接口。
VITE_AIPI_KNOWLEDGE_IFRAME_API_URL: "https://ch1.b.uat.4.cn/knowes/api/iframeKnowledgeUrl",
// 知识库 iframe 皮肤参数。
VITE_AIPI_KNOWLEDGE_IFRAME_SKIN: "red",
// MopeChat 接口地址。
VITE_AIPI_MOPECHAT_URL: "https://ch1.b.uat.4.cn/likun_b1_mope",
// 工作室/空间后端接口:空间列表、空间详情、素材上传等使用。
VITE_AIPI_BACKENP_URL: "http://47.88.25.99:8000",
// 工作室/空间后端别名:兼容旧配置命名,通常与 VITE_AIPI_BACKENP_URL 保持一致。
VITE_NOTEBOOK_API_BASE_URL: "http://47.88.25.99:8000",
// 工作室/空间前端地址:用于 auth/callback、新建对话等跳转。
VITE_AIPI_FRONTEND_URL: "http://47.88.25.99:3000",
// Consumer 接口:深链任务详情 cop-task-detail、研判情报报告 selectSqReport 等使用。
VITE_AIPI_CONSUMER_URL: "http://10.19.200.5:8002/consumer",
// A 系统跳转地址:人物、组织、事件等列表项打开链接使用。
VITE_AIPI_PRESENT_URL: "http://present.a.uat.4.cn/present/domain",
// B 系统 knowes 根地址。
VITE_AIPI_KNOWES_URL: "http://ch1.b.uat.4.cn/knowes",
// MaxKey 鉴权地址:onlineTicket 换 token 使用。
VITE_AUTH_BASE_URL: "https://auth.y.sit.4.cn",
// PG 中任务 iframe 地址。
VITE_AIPI_PGLINKTO_URL: "https://pg.b.uat.4.cn",
// XK 中任务 iframe 地址。
VITE_AIPI_XKLINKTO_URL: "https://kz.b.uat.4.cn",
// 是否使用真实任务看板数据:true=真实接口;false=本地 mock。
VITE_USE_REAL_DATA: "true",
// 登录深链任务详情是否使用本地 mock:false=真实接口;true=本地 mock。
VITE_MOCK_COP_TASK_DETAIL: "false",
// 大屏绘制 selectSqReport 是否使用本地 mock:false=真实接口;true=本地 mock。
VITE_MOCK_SQ_REPORT: "false",
// 静态站点模式开关;一般部署业务系统时保持 false。
VITE_STATIC_WEBSITE_ONLY: "false",
// GitHub OAuth Token;不使用 GitHub 相关能力时留空。
VITE_GITHUB_OAUTH_TOKEN: "",
};
@@ -0,0 +1,109 @@
import fs from "node:fs";
import path from "node:path";
import postcss from "postcss";
const unsupportedColor = /(?:color-mix|oklch|oklab|lab|lch)\(/i;
const assetsDir = path.resolve("dist/assets");
if (!fs.existsSync(assetsDir)) {
throw new Error("dist/assets 不存在,请先运行 pnpm build");
}
const cssFiles = fs
.readdirSync(assetsDir)
.filter((file) => file.endsWith(".css"));
if (cssFiles.length === 0) {
throw new Error("dist/assets 中没有 CSS 构建产物");
}
const failures = [];
let modernDeclarations = 0;
const requiredLegacyFallbacks = [
{
file: "src/components/ui/textarea.tsx",
pattern: /rows=\{rows\s*\?\?\s*3\}/,
description: "Textarea needs a native rows fallback",
},
{
file: "src/components/ai-elements/prompt-input.tsx",
pattern: /<InputGroup className="h-auto min-h-16">/,
description: "Prompt textarea must not rely on :has() for its parent height",
},
];
for (const file of cssFiles) {
const root = postcss.parse(
fs.readFileSync(path.join(assetsDir, file), "utf8"),
{ from: file },
);
root.walkDecls((declaration) => {
if (!unsupportedColor.test(declaration.value)) return;
modernDeclarations += 1;
let ancestor = declaration.parent;
while (ancestor) {
if (
ancestor.type === "atrule" &&
ancestor.name === "supports" &&
unsupportedColor.test(ancestor.params)
) {
return;
}
ancestor = ancestor.parent;
}
let previous = declaration.prev();
while (previous?.type === "comment") previous = previous.prev();
const hasSafeFallback =
!declaration.prop.startsWith("--") &&
previous?.type === "decl" &&
previous.prop === declaration.prop &&
!unsupportedColor.test(previous.value);
if (!hasSafeFallback) {
failures.push(
`${file}:${declaration.source?.start?.line ?? "?"} ${declaration.prop}: ${declaration.value}`,
);
}
});
}
function walk(directory) {
return fs.readdirSync(directory, { withFileTypes: true }).flatMap((entry) => {
const fullPath = path.join(directory, entry.name);
return entry.isDirectory() ? walk(fullPath) : [fullPath];
});
}
for (const file of walk(path.resolve("src"))) {
if (!/\.[jt]sx?$/.test(file)) continue;
const source = fs.readFileSync(file, "utf8");
if (unsupportedColor.test(source)) {
failures.push(
`${path.relative(process.cwd(), file)} contains a runtime-only modern color function`,
);
}
}
for (const fallback of requiredLegacyFallbacks) {
const source = fs.readFileSync(path.resolve(fallback.file), "utf8");
if (!fallback.pattern.test(source)) {
failures.push(`${fallback.file}: ${fallback.description}`);
}
}
if (failures.length > 0) {
console.error("Legacy Chrome CSS compatibility check failed:\n");
console.error(failures.slice(0, 50).join("\n"));
if (failures.length > 50) {
console.error(`... and ${failures.length - 50} more`);
}
process.exit(1);
}
console.log(
`Legacy Chrome (100+) CSS compatibility check passed (${modernDeclarations} guarded modern declarations).`,
);
@@ -0,0 +1,70 @@
/**
* Tailwind 4 gracefully falls back from color-mix() to the base semantic
* color, but that turns utilities such as bg-primary/5 into an opaque fill on
* Chrome 109. Replace that fallback with an sRGB-channel based alpha color.
* The modern color-mix() declaration remains in its @supports block.
*/
const LEGACY_RGB_TOKENS = new Set([
"accent",
"background",
"border",
"brand-200",
"card",
"destructive",
"foreground",
"input",
"muted",
"muted-foreground",
"primary",
"primary-foreground",
"ring",
"secondary",
"sidebar-accent",
"sidebar-border",
"sidebar-foreground",
"workspace-brand",
]);
const TRANSPARENT_MIX_RE =
/color-mix\(\s*in\s+[\w-]+\s*,\s*var\(--([\w-]+)\)\s+([\d.]+)%\s*,\s*transparent\s*\)/i;
function legacyAlphaColor(value) {
const match = value.match(TRANSPARENT_MIX_RE);
if (!match || !LEGACY_RGB_TOKENS.has(match[1])) return null;
const [, token, percentage] = match;
return `rgb(var(--legacy-${token}-rgb) / ${percentage}%)`;
}
export default function legacyColorFallback() {
return {
postcssPlugin: "postcss-legacy-color-fallback",
OnceExit(root) {
root.walkAtRules("supports", (atRule) => {
if (!atRule.params.includes("color-mix")) return;
atRule.walkDecls((modernDeclaration) => {
const fallbackValue = legacyAlphaColor(modernDeclaration.value);
if (!fallbackValue) return;
// Tailwind emits the old-browser declaration immediately before
// the nested @supports block, in the same utility/variant rule.
let fallbackDeclaration = atRule.prev();
while (fallbackDeclaration?.type === "comment") {
fallbackDeclaration = fallbackDeclaration.prev();
}
if (
fallbackDeclaration?.type === "decl" &&
fallbackDeclaration.prop === modernDeclaration.prop
) {
fallbackDeclaration.value = fallbackValue;
}
});
});
},
};
}
legacyColorFallback.postcss = true;
@@ -0,0 +1,33 @@
import postcss from "postcss";
import { transform } from "lightningcss";
const LEGACY_CHROME = 100 << 16;
/**
* Keep Vite's PostCSS pipeline (required by Tailwind 4), then compile the
* resulting CSS to Chrome 100-compatible syntax for both dev and production.
*/
export default function lightningCssLegacyChrome() {
return {
postcssPlugin: "postcss-lightningcss-legacy-chrome",
OnceExit(root, { result }) {
const filename = root.source?.input.file ?? result.opts.from ?? "style.css";
const output = transform({
filename,
code: Buffer.from(root.toString()),
targets: { chrome: LEGACY_CHROME },
minify: false,
});
const compiled = postcss.parse(output.code.toString(), { from: filename });
root.removeAll();
root.append(compiled.nodes);
for (const warning of output.warnings) {
result.warn(warning.message, { plugin: "lightningcss" });
}
},
};
}
lightningCssLegacyChrome.postcss = true;
@@ -846,7 +846,10 @@ export const PromptInput = ({
ref={formRef}
{...props}
>
<InputGroup>{children}</InputGroup>
{/* Do not rely on InputGroup's :has(textarea) selector here: Chrome
versions before 105 ignore :has(), retain h-9, and clip the whole
multi-line composer to a single row. */}
<InputGroup className="h-auto min-h-16">{children}</InputGroup>
</form>
</PromptInputValidationContext.Provider>
);
@@ -873,16 +876,48 @@ export type PromptInputTextareaProps = ComponentProps<
typeof InputGroupTextarea
>;
export const PromptInputTextarea = ({
onChange,
className,
placeholder = "What would you like to know?",
...props
}: PromptInputTextareaProps) => {
const controller = useOptionalPromptInputController();
const attachments = usePromptInputAttachments();
const sanitizeIncomingFiles = usePromptInputValidation();
const [isComposing, setIsComposing] = useState(false);
export const PromptInputTextarea = ({
onChange,
onInput,
className,
placeholder = "What would you like to know?",
...props
}: PromptInputTextareaProps) => {
const controller = useOptionalPromptInputController();
const attachments = usePromptInputAttachments();
const sanitizeIncomingFiles = usePromptInputValidation();
const [isComposing, setIsComposing] = useState(false);
const textareaRef = useRef<HTMLTextAreaElement | null>(null);
const resizeForLegacyChrome = useCallback((textarea: HTMLTextAreaElement) => {
if (
typeof CSS !== "undefined" &&
typeof CSS.supports === "function" &&
CSS.supports("field-sizing", "content")
) {
return;
}
// Chrome 109 ignores field-sizing: content. Preserve the same 4rem–12rem
// autosize range used by min-h-16/max-h-48 instead of leaving the prompt
// stuck at its minimum height.
textarea.style.height = "auto";
const height = Math.max(64, Math.min(textarea.scrollHeight, 192));
textarea.style.height = `${height}px`;
textarea.style.overflowY = textarea.scrollHeight > 192 ? "auto" : "hidden";
}, []);
// onInput does not run for a controlled value filled by a suggested question
// or restored draft. Resize once after every render in legacy Chrome too.
useEffect(() => {
const textarea = textareaRef.current;
if (!textarea) return;
const frame = window.requestAnimationFrame(() => {
resizeForLegacyChrome(textarea);
});
return () => window.cancelAnimationFrame(frame);
}, [controller?.textInput.value, resizeForLegacyChrome]);
const handleKeyDown: KeyboardEventHandler<HTMLTextAreaElement> = (e) => {
if (e.key === "Enter") {
@@ -962,12 +997,18 @@ export const PromptInputTextarea = ({
};
return (
<InputGroupTextarea
className={cn("field-sizing-content max-h-48 min-h-16", className)}
name="message"
<InputGroupTextarea
className={cn("field-sizing-content max-h-48 min-h-16", className)}
name="message"
ref={textareaRef}
rows={3}
onCompositionEnd={() => setIsComposing(false)}
onCompositionStart={() => setIsComposing(true)}
onKeyDown={handleKeyDown}
onKeyDown={handleKeyDown}
onInput={(event) => {
resizeForLegacyChrome(event.currentTarget);
onInput?.(event);
}}
onPaste={handlePaste}
placeholder={placeholder}
{...props}
@@ -26,6 +26,7 @@ import {
import { Button } from "@/components/ui/button";
import { toast } from "sonner";
import { getStoredAuth } from "@/core/auth";
import { readRuntimeEnv } from "@/env";
import { queryClient } from "@/components/query-client-provider";
import { Checkbox } from "@/components/ui/checkbox";
import Empty from "./empty";
@@ -303,11 +304,11 @@ const DocumentUploadPopupContent: FC<{
const searchSpaceId = String(searchSpace?.id);
// 从环境变量获取配置
const backendUrl = import.meta.env.VITE_AIPI_BACKENP_URL || "http://localhost:8001";
const consumerUrl = import.meta.env.VITE_AIPI_CONSUMER_URL || "http://ch1.b.uat.4.cn/consumer";
const presentUrl = import.meta.env.VITE_AIPI_PRESENT_URL || "http://present.a.uat.4.cn/present/domain";
const knowesUrl = import.meta.env.VITE_AIPI_KNOWES_URL || "http://ch1.b.uat.4.cn/knowes";
const knowledgeUrl = import.meta.env.VITE_AIPI_KNOWLEDGE_URL || "http://chlib.b.uat.4.cn/knowlibrary";
const backendUrl = readRuntimeEnv("VITE_AIPI_BACKENP_URL") || "http://localhost:8001";
const consumerUrl = readRuntimeEnv("VITE_AIPI_CONSUMER_URL") || "http://ch1.b.uat.4.cn/consumer";
const presentUrl = readRuntimeEnv("VITE_AIPI_PRESENT_URL") || "http://present.a.uat.4.cn/present/domain";
const knowesUrl = readRuntimeEnv("VITE_AIPI_KNOWES_URL") || "http://ch1.b.uat.4.cn/knowes";
const knowledgeUrl = readRuntimeEnv("VITE_AIPI_KNOWLEDGE_URL") || "http://chlib.b.uat.4.cn/knowlibrary";
const [searchText, setSearchText] = useState("");
const [searchValue, setSearchValue] = useState("");
@@ -11,6 +11,7 @@ import { Button } from "@/components/ui/button";
import { toast } from "sonner";
import { getStoredAuth } from "@/core/auth";
import { authenticatedFetch } from "@/components/assistant-ui/document-upload-popup";
import { readRuntimeEnv } from "@/env";
interface NetworkActorProps {
@@ -186,9 +187,9 @@ export function NetworkActor({ inputValue, searchSpaceId, onSuccess }: NetworkAc
const [actorCount, setActorCount] = useState<number>(0);
const [directionType, setDirectionType] = useState<string>('TW');
const directionValue = useRef<string>('TW');
const consumerUrl = import.meta.env.VITE_AIPI_CONSUMER_URL || 'https://ch1.b.uat.4.cn/consumer';
const consumerUrl = readRuntimeEnv("VITE_AIPI_CONSUMER_URL") || 'https://ch1.b.uat.4.cn/consumer';
const backendUrl = typeof window !== 'undefined'
? (import.meta.env.VITE_BACKEND_BASE_URL || 'http://localhost:8001')
? (readRuntimeEnv("VITE_BACKEND_BASE_URL") || 'http://localhost:8001')
: 'http://localhost:8001';
const [selectedSearchSources, setSelectedSearchSources] = useState<UserDataItem[]>([]);
const [isImporting, setIsImporting] = useState(false);
@@ -508,8 +508,9 @@ export default function ProgressiveSkillsAnimation() {
"deploying",
"done",
].includes(phase) && (
<div className="text-base text-zinc-300">
<span className="text-purple-400">✨</span> Found 3 skills
<div className="flex items-center gap-2 text-base text-zinc-300">
<Sparkles size={16} className="text-purple-400" />
<span>Found 3 skills</span>
</div>
)}
@@ -526,8 +527,9 @@ export default function ProgressiveSkillsAnimation() {
].includes(phase) && (
<div className="mt-4">
<hr className="mb-3 border-zinc-700" />
<div className="mb-3 text-zinc-300">
🔬 Researching...
<div className="mb-3 inline-flex items-center gap-2 text-zinc-300">
<Search size={16} className="text-blue-400" />
<span>Researching...</span>
</div>
<div className="mb-3 space-y-2">
{/* Loading SKILL.md */}
@@ -623,7 +625,10 @@ export default function ProgressiveSkillsAnimation() {
className="mt-4"
>
<hr className="mb-3 border-zinc-700" />
<div className="mb-3 text-zinc-300">🔨 Building...</div>
<div className="mb-3 inline-flex items-center gap-2 text-zinc-300">
<FileText size={16} className="text-zinc-400" />
<span>Building...</span>
</div>
<div className="mb-3 flex items-center gap-2 pl-4 text-zinc-400">
<FileText size={16} />
<span>Loading frontend-design/SKILL.md...</span>
@@ -653,7 +658,10 @@ export default function ProgressiveSkillsAnimation() {
className="mt-4"
>
<hr className="mb-3 border-zinc-700" />
<div className="mb-3 text-zinc-300">🚀 Deploying...</div>
<div className="mb-3 inline-flex items-center gap-2 text-zinc-300">
<Terminal size={16} className="text-zinc-400" />
<span>Deploying...</span>
</div>
<div className="mb-3 space-y-2">
<div className="flex items-center gap-2 pl-4 text-zinc-400">
<FileText size={16} />
@@ -676,8 +684,9 @@ export default function ProgressiveSkillsAnimation() {
animate={{ opacity: 1, scale: 1 }}
className="mt-4 rounded-xl border border-green-500/30 bg-green-500/10 p-4"
>
<div className="text-lg font-medium text-green-500">
✅ Live at biotech-startup.vercel.app
<div className="inline-flex items-center gap-2 text-lg font-medium text-green-500">
<Check size={18} />
<span>Live at biotech-startup.vercel.app</span>
</div>
</motion.div>
)}
@@ -35,27 +35,27 @@ export function SandboxSection({ className }: { className?: string }) {
$ pip install -r requirements.txt
</TypingAnimation>
<AnimatedSpan delay={2000} className="text-green-500">
✔ Installed pygame
[ok] Installed pygame
</AnimatedSpan>
<TypingAnimation delay={2400}>
$ write game.py --lines 156
</TypingAnimation>
<AnimatedSpan delay={3200} className="text-blue-500">
✔ Written 156 lines
[ok] Written 156 lines
</AnimatedSpan>
<TypingAnimation delay={3600}>
$ python game.py --test
</TypingAnimation>
<AnimatedSpan delay={4200} className="text-green-500">
✔ All sprites loaded
[ok] All sprites loaded
</AnimatedSpan>
<AnimatedSpan delay={4500} className="text-green-500">
✔ Physics engine OK
[ok] Physics engine OK
</AnimatedSpan>
<AnimatedSpan delay={4800} className="text-green-500">
✔ 60 FPS stable
[ok] 60 FPS stable
</AnimatedSpan>
{/* Scene 2: Data Analysis */}
+13 -10
View File
@@ -144,21 +144,24 @@ function InputGroupInput({
);
}
function InputGroupTextarea({
className,
...props
}: React.ComponentProps<"textarea">) {
const InputGroupTextarea = React.forwardRef<
HTMLTextAreaElement,
React.ComponentProps<"textarea">
>(function InputGroupTextarea({ className, ...props }, ref) {
return (
<Textarea
data-slot="input-group-control"
<Textarea
data-slot="input-group-control"
ref={ref}
className={cn(
"flex-1 resize-none rounded-none border-0 bg-transparent py-3 shadow-none focus-visible:ring-0 dark:bg-transparent",
className,
)}
{...props}
/>
);
}
{...props}
/>
);
});
InputGroupTextarea.displayName = "InputGroupTextarea";
export {
InputGroup,
+19 -10
View File
@@ -2,19 +2,28 @@ import * as React from "react";
import { cn } from "@/lib/utils";
function Textarea({ className, ...props }: React.ComponentProps<"textarea">) {
return (
<textarea
data-slot="textarea"
className={cn(
"border-input placeholder:text-muted-foreground/60 flex field-sizing-content min-h-16 w-full rounded-md border bg-transparent px-3 py-2 text-base shadow-xs transition-[color,box-shadow] outline-none disabled:cursor-not-allowed disabled:opacity-50 md:text-sm",
const Textarea = React.forwardRef<
HTMLTextAreaElement,
React.ComponentProps<"textarea">
>(function Textarea({ className, rows, ...props }, ref) {
// `field-sizing` is not available in older Chrome. Keep a native rows
// fallback so a multi-line editor never collapses into a single line.
return (
<textarea
data-slot="textarea"
ref={ref}
rows={rows ?? 3}
className={cn(
"border-input placeholder:text-muted-foreground/60 flex min-h-16 w-full rounded-md border bg-transparent px-3 py-2 text-base shadow-xs transition-[color,box-shadow] outline-none disabled:cursor-not-allowed disabled:opacity-50 md:text-sm",
"dark:border-[#1B477E] dark:bg-[#022248] dark:text-[#AACEFF] ",
"focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive",
className,
)}
{...props}
/>
);
}
{...props}
/>
);
});
Textarea.displayName = "Textarea";
export { Textarea };
@@ -3,7 +3,7 @@ export function EmptyDocumentState() {
return (
<div className="flex h-full items-center justify-center bg-white px-6 text-center dark:bg-slate-900">
<div className="rounded-2xl border border-dashed border-slate-300 bg-slate-50 px-10 py-12 sm:px-12 dark:border-slate-700 dark:bg-slate-800/50">
<div className="mb-4 text-[40px] leading-none">📚</div>
<div className="mb-4 text-[28px] font-semibold leading-none text-slate-500 dark:text-slate-300">文档</div>
<div className="mb-2 text-lg font-bold text-slate-700 dark:text-slate-200">
选择一篇文档开始阅读
</div>
@@ -213,7 +213,7 @@ const errorToastDarkOptions = {
</div>
<div className="flex flex-1 flex-wrap items-center gap-1.5">
{summary && Object.keys(summary.by_category).length === 0 && (
<span className="text-muted-foreground text-sm">暂无失败记录 🎉</span>
<span className="text-muted-foreground text-sm">暂无失败记录</span>
)}
{summary &&
Object.entries(summary.by_category)
@@ -35,7 +35,7 @@ export function AgentWelcome({
className,
)}
>
<div className="bg-primary/10 ring-primary/15 mb-2 flex h-[72px] w-[72px] items-center justify-center rounded-3xl ring-1">
<div className="agent-avatar bg-primary/10 ring-primary/15 mb-2 flex h-[72px] w-[72px] items-center justify-center rounded-3xl ring-1">
<BotIcon className="text-primary h-9 w-9" />
</div>
<div className="text-3xl font-bold tracking-tight">
@@ -839,7 +839,7 @@ export function AgentCard({ agent, layout = "card", onPublisherClick }: AgentCar
<CardHeader className="min-w-0 pb-2">
<div className="flex min-w-0 items-start justify-between gap-2">
<div className="flex min-w-0 flex-1 items-center gap-2 overflow-hidden">
<div className="bg-primary/10 text-primary flex h-9 w-9 shrink-0 items-center justify-center rounded-lg">
<div className="agent-avatar bg-primary/10 text-primary flex h-9 w-9 shrink-0 items-center justify-center rounded-lg">
<BotIcon className="h-5 w-5" />
</div>
<div className="min-w-0 flex-1 overflow-hidden">
@@ -894,7 +894,7 @@ export function AgentCard({ agent, layout = "card", onPublisherClick }: AgentCar
const listLayout = (
<div className="group flex min-w-0 items-center gap-3 rounded-lg border p-2.5 transition-shadow hover:shadow-sm dark:border-[#213D5E] dark:bg-[#02244C]">
<div className="bg-primary/10 text-primary flex h-9 w-9 shrink-0 items-center justify-center rounded-lg">
<div className="agent-avatar bg-primary/10 text-primary flex h-9 w-9 shrink-0 items-center justify-center rounded-lg">
<BotIcon className="h-5 w-5" />
</div>
<div className="min-w-0 flex-1 overflow-hidden">
@@ -980,7 +980,7 @@ export function AgentCard({ agent, layout = "card", onPublisherClick }: AgentCar
? "正在加载 SOUL.md..."
: "输入 SOUL.md 内容"
}
className="max-h-[45vh] min-h-48 resize-y font-mono text-xs"
className="max-h-[45vh] min-h-48 resize-y text-xs"
/>
</div>
<div className="space-y-2">
@@ -264,7 +264,7 @@ export function AgentChatSidebar({ activeAgentId }: { activeAgentId?: string })
isActive
? isDark
? "border-[#2b8ffe] bg-[#0b4fae]/65 shadow-[0_0_0_1px_rgba(43,143,254,0.5),0_12px_28px_rgba(37,128,255,0.25)]"
: "border-primary/50 bg-primary/10 shadow-sm"
: "agent-sidebar-card--active border-primary/50 bg-primary/10 shadow-sm"
: isDark
? "border-[#2b8ffe]/20 bg-[#042b59]/35 hover:border-[#2b8ffe]/60 hover:bg-[#063b77]/60"
: "border-transparent hover:border-border hover:bg-accent/50",
@@ -520,7 +520,7 @@ export function AgentDetailPanel({
{/* ── Identity / 基础信息 ─────────────────────────────────────────── */}
<section className="bg-card/60 rounded-xl border p-4">
<div className="flex items-start gap-3">
<div className="bg-primary/10 ring-primary/15 flex size-12 shrink-0 items-center justify-center rounded-2xl ring-1">
<div className="agent-avatar bg-primary/10 ring-primary/15 flex size-12 shrink-0 items-center justify-center rounded-2xl ring-1">
<BotIcon className="text-primary size-6" />
</div>
<div className="min-w-0 flex-1 space-y-1.5">
@@ -830,8 +830,8 @@ export function AgentDetailPanel({
const panelChrome = (collapseButton: ReactNode) => (
<>
<div className="flex h-12 shrink-0 items-center justify-between border-b px-4">
<span className="text-sm font-semibold">智能体信息</span>
<div className="flex items-center gap-1">
<span className="shrink-0 whitespace-nowrap text-sm font-semibold">智能体信息</span>
<div className="flex min-w-0 items-center gap-1">
{headerActions}
{compactAppearanceActions}
{collapseButton}
@@ -138,7 +138,7 @@ export function AgentModuleSettingsDialog({
key={agent.id}
className="flex items-start gap-3 rounded-xl border border-border/60 bg-card p-3"
>
<div className="bg-primary/10 text-primary mt-0.5 flex size-8 shrink-0 items-center justify-center rounded-lg">
<div className="agent-avatar bg-primary/10 text-primary mt-0.5 flex size-8 shrink-0 items-center justify-center rounded-lg">
<Bot className="size-4" />
</div>
<div className="min-w-0 flex-1">
@@ -223,7 +223,7 @@ export function AgentModuleSidebar({
isActive
? isDark
? "border-[#2b8ffe] bg-[#0b4fae]/65 shadow-[0_0_0_1px_rgba(43,143,254,0.5),0_12px_28px_rgba(37,128,255,0.25)]"
: "border-primary/50 bg-primary/10 shadow-sm"
: "agent-sidebar-card--active border-primary/50 bg-primary/10 shadow-sm"
: isDark
? "border-[#2b8ffe]/20 bg-[#042b59]/35 hover:border-[#2b8ffe]/60 hover:bg-[#063b77]/60"
: "border-transparent hover:border-border hover:bg-accent/50",
@@ -20,16 +20,16 @@ export function CompactWelcome({ className }: { className?: string }) {
className="mb-3.5 inline-flex items-center justify-center rounded-full p-3"
style={{
backgroundColor:
"color-mix(in srgb, var(--workspace-brand) 5%, transparent)",
"rgb(var(--legacy-workspace-brand-rgb) / 5%)",
border:
"1px solid color-mix(in srgb, var(--workspace-brand) 12%, transparent)",
"1px solid rgb(var(--legacy-workspace-brand-rgb) / 12%)",
}}
>
<div
className="flex size-14 items-center justify-center rounded-2xl shadow-md"
style={{
backgroundImage:
"linear-gradient(to top right, var(--workspace-brand), color-mix(in srgb, var(--workspace-brand) 55%, white))",
"linear-gradient(to top right, var(--workspace-brand), var(--brand-200))",
}}
>
<span className="text-2xl font-black tracking-wide text-white italic">
@@ -38,8 +38,8 @@ export function CompactWelcome({ className }: { className?: string }) {
</div>
</div>
<h1 className="flex items-center justify-center gap-2 text-2xl font-bold tracking-tight text-foreground/90">
<span className="text-2xl">👋</span> 欢迎使用CM助手
<h1 className="flex items-center justify-center text-2xl font-bold tracking-tight text-foreground/90">
欢迎使用CM助手
</h1>
<p className="text-muted-foreground mx-auto mt-2 max-w-sm text-xs leading-relaxed">
基于先进深度推理模型,为您提供高效的市场分析、专业报告撰写、调研设计与协同脑暴支持。
@@ -320,7 +320,7 @@ export function GeneralQaSidebar() {
className={cn(
"flex h-10 min-w-0 flex-1 items-center justify-center gap-1.5 rounded-[5px] text-[14px] font-semibold transition-colors",
"border border-solid",
"bg-[color-mix(in_srgb,var(--primary)_14%,transparent)] hover:bg-[color-mix(in_srgb,var(--primary)_24%,transparent)]",
"bg-[rgb(var(--legacy-primary-rgb)/14%)] hover:bg-[rgb(var(--legacy-primary-rgb)/24%)]",
)}
>
<Plus className="size-4 stroke-[2.5]" />
@@ -353,7 +353,7 @@ export function GeneralQaSidebar() {
isActive
? isDark
? "border-[#2b8ffe] bg-[#0b4fae]/65 shadow-[0_0_0_1px_rgba(43,143,254,0.5),0_12px_28px_rgba(37,128,255,0.25)]"
: "border-primary/50 bg-primary/10 shadow-sm"
: "agent-sidebar-card--active border-primary/50 bg-primary/10 shadow-sm"
: isDark
? "border-[#2b8ffe]/20 bg-[#042b59]/35 hover:border-[#2b8ffe]/60 hover:bg-[#063b77]/60"
: "border-transparent hover:border-border hover:bg-accent/50",
@@ -613,7 +613,7 @@ export function InputBox({
: "*:data-[slot='input-group']:rounded-3xl",
// 紧凑落地态(CM 参考图):白卡 + 细边 + 轻阴影 + 主色聚焦环(无旋转彩边)
compact && isNewThread
? "rounded-2xl border border-gray-200/70 bg-white overflow-hidden shadow-[0_4.5px_22px_rgba(0,0,0,0.025)] transition-all duration-300 focus-within:border-[color-mix(in_srgb,var(--primary)_35%,transparent)] focus-within:ring-4 focus-within:ring-[color-mix(in_srgb,var(--primary)_5%,transparent)] focus-within:shadow-[0_10px_35px_color-mix(in_srgb,var(--primary)_4%,transparent)] dark:border-slate-700 dark:bg-slate-800"
? "rounded-2xl border border-gray-200/70 bg-white overflow-hidden shadow-[0_4.5px_22px_rgba(0,0,0,0.025)] transition-all duration-300 focus-within:border-[rgb(var(--legacy-primary-rgb)/35%)] focus-within:ring-4 focus-within:ring-[rgb(var(--legacy-primary-rgb)/5%)] focus-within:shadow-[0_10px_35px_rgb(var(--legacy-primary-rgb)/4%)] dark:border-slate-700 dark:bg-slate-800"
: // landing 态:透明,让外层 gemini-gradient-border 提供白卡 + 旋转描边
// 对话态:常规白卡 + 中性边框 + 轻阴影
isNewThread
@@ -680,18 +680,21 @@ export function InputBox({
<PromptInputButton
className={cn(
"gap-1.5! rounded-full px-3! shadow-none",
compact && "rounded-lg px-2.5! py-1! text-[11px] font-medium",
context.writing_mode
? "border border-amber-300 bg-amber-50 text-amber-700 hover:bg-amber-100 dark:border-amber-700 dark:bg-amber-950/40 dark:text-amber-200"
: compact
? "border border-transparent bg-muted/40 text-muted-foreground hover:bg-muted hover:text-foreground"
: "border-0 bg-transparent text-muted-foreground hover:bg-muted/70 hover:text-foreground dark:text-slate-400 dark:hover:bg-slate-700/70 dark:hover:text-slate-100",
)}
onClick={handleWritingModeToggle}
>
<FileTextIcon className="size-3" />
<span className="text-xs font-normal">写作</span>
</PromptInputButton>
compact && "rounded-lg px-2.5! py-1! text-[11px] font-medium",
context.writing_mode
? "border border-amber-300 bg-amber-50 text-amber-700 ring-1 ring-amber-300/35 hover:bg-amber-100 dark:border-amber-300/70 dark:bg-amber-400/20 dark:text-amber-100 dark:ring-amber-300/35 dark:shadow-[0_0_0_1px_rgba(251,191,36,0.18),0_0_18px_rgba(251,191,36,0.18)] dark:hover:bg-amber-400/25"
: compact
? "border border-transparent bg-muted/40 text-muted-foreground hover:bg-muted hover:text-foreground"
: "border-0 bg-transparent text-muted-foreground hover:bg-muted/70 hover:text-foreground dark:text-slate-400 dark:hover:bg-slate-700/70 dark:hover:text-slate-100",
)}
onClick={handleWritingModeToggle}
>
{context.writing_mode && (
<CheckIcon className="size-3 text-amber-700 dark:text-amber-100" />
)}
<FileTextIcon className="size-3" />
<span className="text-xs font-normal">写作</span>
</PromptInputButton>
</Tooltip>
<Tooltip
content={
@@ -702,19 +705,22 @@ export function InputBox({
>
<PromptInputButton
className={cn(
"gap-1.5! rounded-full px-3! shadow-none",
compact && "rounded-lg px-2.5! py-1! text-[11px] font-medium",
memoryInjectionEnabled
? "border border-blue-300 bg-blue-50 text-blue-700 hover:bg-blue-100 dark:border-blue-700 dark:bg-blue-950/40 dark:text-blue-200"
: compact
? "border border-transparent bg-muted/40 text-muted-foreground hover:bg-muted hover:text-foreground"
: "border-0 bg-transparent text-muted-foreground hover:bg-muted/70 hover:text-foreground dark:text-slate-400 dark:hover:bg-slate-700/70 dark:hover:text-slate-100",
)}
onClick={handleMemoryInjectionToggle}
>
<BrainCircuitIcon className="size-3" />
<span className="text-xs font-normal">记忆</span>
</PromptInputButton>
"gap-1.5! rounded-full px-3! shadow-none",
compact && "rounded-lg px-2.5! py-1! text-[11px] font-medium",
memoryInjectionEnabled
? "border border-blue-300 bg-blue-50 text-blue-700 ring-1 ring-blue-300/35 hover:bg-blue-100 dark:border-cyan-300/70 dark:bg-cyan-400/20 dark:text-cyan-50 dark:ring-cyan-300/35 dark:shadow-[0_0_0_1px_rgba(34,211,238,0.18),0_0_18px_rgba(34,211,238,0.18)] dark:hover:bg-cyan-400/25"
: compact
? "border border-transparent bg-muted/40 text-muted-foreground hover:bg-muted hover:text-foreground"
: "border-0 bg-transparent text-muted-foreground hover:bg-muted/70 hover:text-foreground dark:text-slate-400 dark:hover:bg-slate-700/70 dark:hover:text-slate-100",
)}
onClick={handleMemoryInjectionToggle}
>
{memoryInjectionEnabled && (
<CheckIcon className="size-3 text-blue-700 dark:text-cyan-50" />
)}
<BrainCircuitIcon className="size-3" />
<span className="text-xs font-normal">记忆</span>
</PromptInputButton>
</Tooltip>
<Tooltip
content={
@@ -1166,7 +1172,7 @@ export function InputBox({
className={cn(
"rounded-full border-border bg-background shadow-sm hover:bg-muted dark:bg-blue-500 dark:border-blue-500 dark:text-white dark:hover:bg-blue-400 dark:shadow-none",
compact &&
"size-7 rounded-lg border-0 bg-[var(--primary)] text-[var(--primary-foreground)] hover:bg-[color-mix(in_srgb,var(--primary)_88%,black)] disabled:bg-muted disabled:text-muted-foreground/40 dark:bg-[var(--primary)] dark:border-0 dark:text-[var(--primary-foreground)] dark:hover:bg-[color-mix(in_srgb,var(--primary)_88%,black)] dark:disabled:bg-muted",
"size-7 rounded-lg border-0 bg-[var(--primary)] text-[var(--primary-foreground)] hover:opacity-90 disabled:bg-muted disabled:text-muted-foreground/40 dark:bg-[var(--primary)] dark:border-0 dark:text-[var(--primary-foreground)] dark:hover:opacity-90 dark:disabled:bg-muted",
)}
disabled={disabled}
variant="outline"
@@ -112,7 +112,7 @@ export function RecentRoundtableList() {
}
try {
await rename(id, title);
toast.success("✏️ 已重命名");
toast.success("已重命名");
} catch {
toast.error("重命名失败,请重试");
} finally {
@@ -126,7 +126,7 @@ export function RecentRoundtableList() {
if (!window.confirm("确定删除这条会商记录吗?此操作不可恢复。")) return;
try {
await remove(draft.id);
toast.success("🗑 已删除会商记录");
toast.success("已删除会商记录");
} catch {
toast.error("删除失败,请重试");
}
@@ -8,7 +8,7 @@ CM Assistant is an internal super-agent platform that orchestrates sub-agents, m
---
## 🚀 Core Features
## Core Features
* **Skills & Tools**: With built-in and extensible skills and tools, CM Assistant can do almost anything.
* **Sub-Agents**: Sub-Agents help the main agent to do the tasks that are too complex to be done by the main agent.
@@ -1,12 +1,12 @@
"use client";
import { useMemo } from "react";
import { CopyIcon, LogOutIcon, UserIcon } from "lucide-react";
import { toast } from "sonner";
import { Badge } from "@/components/ui/badge";
import { Button } from "@/components/ui/button";
import { clearStoredAuth, getAccountDisplayName, getStoredAuth } from "@/core/auth";
import { useLiveAccount } from "@/core/auth/use-live-account";
import { OptType, writeYLog } from "@/core/log/y-log";
import { clearYLogToken } from "@/core/log/y-log-token";
@@ -27,7 +27,10 @@ function copy(text: string, hint = "已复制") {
}
export function AccountSettingsPage() {
const auth = useMemo(() => getStoredAuth(), []);
// Subscribe to same-tab and cross-tab auth changes. The dialog may stay open
// while the active account is switched elsewhere.
useLiveAccount();
const auth = getStoredAuth();
if (!auth) {
return (
@@ -1,35 +1,28 @@
"use client";
import { User } from "lucide-react";
import { useMemo } from "react";
import { getStoredAuth } from "@/core/auth";
import { useLiveAccount } from "@/core/auth/use-live-account";
import { useMyPosition } from "@/core/positions";
import { cn } from "@/lib/utils";
import { readUserInfo } from "@/strategy-components/lib/light-app";
/**
* 名称 + 岗位 用户标识。全量模式由 strategy-components/Header.tsx 顶栏右侧展示;
* 紧凑模式(不带全局顶栏)无处承载,故抽成共享组件,复用同一套数据来源
* (`readUserInfo().userName` 优先、回退 auth 邮箱前缀;岗位取 `useMyPosition`),
* (实时 auth 展示名;岗位取 `useMyPosition`),
* 供页面级顶栏(如 ChatPage)右侧复用,保证两套布局一致。
*
* 配色按当前内容区前景色(foreground / muted-foreground),适配聊天页浅底顶栏,
* 而非 Header 的白字贴 banner 方案。
*/
export function UserIdentityBadge({ className }: { className?: string }) {
const auth = getStoredAuth();
// userInfo belongs to external-app route parameters and can legitimately
// outlive a login switch. The visible account name must instead match the
// settings page's current auth identity.
const liveAccount = useLiveAccount();
const { data: myPosition } = useMyPosition();
const positionName = myPosition?.name?.trim() || "";
// 与 Header.tsx 一致:外部登录写入的真实用户名优先,缺省回退邮箱前缀。
const displayName = useMemo(() => {
const info = readUserInfo();
const name = typeof info.userName === "string" ? info.userName.trim() : "";
if (name) return name;
const email = auth?.email?.trim();
return email ? email.split("@")[0] : "用户";
}, [auth?.email]);
const displayName = liveAccount.displayName;
return (
<div
@@ -1,16 +1,14 @@
"use client";
import { useSearchParams } from "next/navigation";
import { useEffect, useMemo } from "react";
import { useMemo } from "react";
import { useI18n } from "@/core/i18n/hooks";
import { cn } from "@/lib/utils";
import { AuroraText } from "../ui/aurora-text";
let waved = false;
export function Welcome({
export function Welcome({
className,
mode,
}: {
@@ -26,10 +24,7 @@ export function Welcome({
}
return ["var(--color-foreground)"];
}, [isUltra]);
useEffect(() => {
waved = true;
}, []);
return (
return (
<div
className={cn(
"mx-auto flex w-full flex-col items-center justify-center gap-2 px-8 py-4 text-center",
@@ -37,15 +32,12 @@ export function Welcome({
)}
>
<div className="text-[26px] font-medium tracking-tight text-foreground/85">
{searchParams.get("mode") === "skill" ? (
`✨ ${t.welcome.createYourOwnSkill} ✨`
) : (
<div className="flex items-center gap-2.5">
<div className={cn("inline-block text-2xl", !waved ? "animate-wave" : "")}>
{isUltra ? "🚀" : "👋"}
</div>
<AuroraText colors={colors}>{t.welcome.greeting}</AuroraText>
</div>
{searchParams.get("mode") === "skill" ? (
t.welcome.createYourOwnSkill
) : (
<div className="flex items-center">
<AuroraText colors={colors}>{t.welcome.greeting}</AuroraText>
</div>
)}
</div>
{searchParams.get("mode") === "skill" ? (
@@ -36,8 +36,8 @@ const NEW_CHAT_CLASS =
// 紧凑模式(CM 参考图):白卡描边、居中、主色 + 图标
const NEW_CHAT_COMPACT_CLASS =
"h-9 justify-center gap-2 rounded-xl bg-card text-[13px] font-semibold text-foreground transition-colors " +
"border border-[color-mix(in_srgb,var(--primary)_18%,transparent)] " +
"hover:bg-[color-mix(in_srgb,var(--primary)_4%,transparent)] hover:text-[var(--primary)] hover:border-[color-mix(in_srgb,var(--primary)_32%,transparent)] " +
"border border-[rgb(var(--legacy-primary-rgb)/18%)] " +
"hover:bg-[rgb(var(--legacy-primary-rgb)/4%)] hover:text-[var(--primary)] hover:border-[rgb(var(--legacy-primary-rgb)/32%)] " +
"data-[active=true]:bg-card data-[active=true]:text-[var(--primary)] data-[active=true]:shadow-none";
export function WorkspaceNavChatList({
@@ -18,6 +18,7 @@
import { requestCrudService } from "@/service/request-axios-service";
import { readCopTaskDetail } from "@/core/auth/task-deeplink";
import { readRuntimeEnv, readRuntimeFlag } from "@/env";
import { fetchTaskDeeplinkConfig } from "@/core/auth/task-deeplink-config";
/** 本页输入 JSON 暂存的 sessionStorage key(拉取一次后复用,避免重复请求)。 */
@@ -89,7 +90,7 @@ export const DEMO_DASHBOARD_INPUT = {
/** 「研判情报报告」接口完整地址:`{VITE_AIPI_CONSUMER_URL}/cop/selectSqReport`。 */
function sqReportUrl(): string {
const value = (import.meta.env as Record<string, unknown>).VITE_AIPI_CONSUMER_URL;
const value = readRuntimeEnv("VITE_AIPI_CONSUMER_URL");
const base = typeof value === "string" ? value.replace(/\/+$/, "") : "";
return base ? `${base}/cop/selectSqReport` : "";
}
@@ -140,7 +141,7 @@ export function extractSqReport(body: unknown, taskId: string): unknown | null {
* 避免开发反复请求样例假数据。真实接口模式(false)下不读不写缓存,**始终拉取真实数据**。
*/
function inputCacheEnabled(): boolean {
return (import.meta.env as Record<string, unknown>).VITE_MOCK_SQ_REPORT === "true";
return readRuntimeFlag("VITE_MOCK_SQ_REPORT");
}
/** 读取暂存的输入 JSON(仅 mock 开发模式生效,真实模式恒返回 null 以强制重新拉取)。 */
+4
View File
@@ -22,6 +22,8 @@ import {
} from "@/core/auth/scoped-storage";
const AUTH_STORAGE_KEY = "deerflow.auth";
/** Fired in the current tab because the browser `storage` event only reaches other tabs. */
export const AUTH_STATE_CHANGED_EVENT = "deerflow:auth-state-changed";
function isBrowser(): boolean {
return typeof window !== "undefined";
@@ -48,6 +50,7 @@ export function setStoredAuth(auth: LoginByUsernameResponse) {
return;
}
authStorageSet(AUTH_STORAGE_KEY, JSON.stringify(auth));
window.dispatchEvent(new Event(AUTH_STATE_CHANGED_EVENT));
}
export function clearStoredAuth() {
@@ -55,6 +58,7 @@ export function clearStoredAuth() {
return;
}
authStorageRemove(AUTH_STORAGE_KEY);
window.dispatchEvent(new Event(AUTH_STATE_CHANGED_EVENT));
}
/** 与设置 → 个人信息页「用户名」一致:邮箱 @ 前缀,否则 user_id。 */
+3 -2
View File
@@ -18,6 +18,7 @@
import { requestCrudService } from "@/service/request-axios-service";
import { clearTaskContext, persistTaskContext } from "@/core/auth/task-purpose";
import { fetchTaskDeeplinkConfig } from "@/core/auth/task-deeplink-config";
import { readRuntimeEnv, readRuntimeFlag } from "@/env";
import { listBusinessMappings } from "@/strategy-components/api/business-mapping";
/** xdfx 业务码:深链传入的是「行动id」,需先调 action 接口解析出真正的「任务id」。 */
@@ -73,13 +74,13 @@ export function parseGoPath(goPath: string): ParsedGoPath | null {
}
function consumerBaseUrl(): string {
const value = import.meta.env.VITE_AIPI_CONSUMER_URL;
const value = readRuntimeEnv("VITE_AIPI_CONSUMER_URL");
return typeof value === "string" ? value.replace(/\/+$/, "") : "";
}
/** cop-task-detail 是否走本地假数据:开关 `VITE_MOCK_COP_TASK_DETAIL=true`(真实接口未就绪时调试用)。 */
function copTaskDetailMockEnabled(): boolean {
return (import.meta.env as Record<string, unknown>).VITE_MOCK_COP_TASK_DETAIL === "true";
return readRuntimeFlag("VITE_MOCK_COP_TASK_DETAIL");
}
/** cop-task-detail 本地假数据(与真实 data 字段同构;mock 开关开启时返回)。 */
@@ -2,7 +2,11 @@
import { useEffect, useRef, useState } from "react";
import { getAccountDisplayName, getStoredAuth } from "@/core/auth";
import {
AUTH_STATE_CHANGED_EVENT,
getAccountDisplayName,
getStoredAuth,
} from "@/core/auth";
import { isEmbedActive } from "@/core/embed/embed-session";
const AUTH_STORAGE_KEY = "deerflow.auth";
@@ -61,11 +65,13 @@ export function useLiveAccount(): LiveAccount {
if (!isEmbedActive()) {
window.addEventListener("storage", onStorage);
}
window.addEventListener(AUTH_STATE_CHANGED_EVENT, refresh);
window.addEventListener("focus", refresh);
document.addEventListener("visibilitychange", refresh);
refresh();
return () => {
window.removeEventListener("storage", onStorage);
window.removeEventListener(AUTH_STATE_CHANGED_EVENT, refresh);
window.removeEventListener("focus", refresh);
document.removeEventListener("visibilitychange", refresh);
};
@@ -5,12 +5,13 @@ import {
persistExternalLoginParams,
} from "./external-login";
import { authStorageSet } from "@/core/auth/scoped-storage";
import { readRuntimeEnv } from "@/env";
const AUTH_BASE_URL = (import.meta.env.VITE_AUTH_BASE_URL || "").replace(
const AUTH_BASE_URL = (readRuntimeEnv("VITE_AUTH_BASE_URL") || "").replace(
/\/$/,
"",
);
const CONSUMER_URL = (import.meta.env.VITE_AIPI_CONSUMER_URL || "").replace(
const CONSUMER_URL = (readRuntimeEnv("VITE_AIPI_CONSUMER_URL") || "").replace(
/\/$/,
"",
);
+2 -1
View File
@@ -1,5 +1,6 @@
import { requestCrudService } from "@/service/request-axios-service";
import { authStorageGet, authStorageSet } from "@/core/auth/scoped-storage";
import { readRuntimeEnv } from "@/env";
import {
deleteExternalAccessTokenFields,
normalizeExternalAccessToken,
@@ -9,7 +10,7 @@ import {
writeStoredExternalAccessToken,
} from "@/core/auth/external-access-token";
const CONSUMER_URL = import.meta.env.VITE_AIPI_CONSUMER_URL || "";
const CONSUMER_URL = readRuntimeEnv("VITE_AIPI_CONSUMER_URL") || "";
export const TOOLS_TOKEN_STORAGE_KEY = "tools_token";
export const EXTERNAL_USER_ID_STORAGE_KEY = "userId";
@@ -2,13 +2,14 @@
import { authStorageGet } from "@/core/auth/scoped-storage";
import { readCurrentExternalAccessTokenValue } from "@/core/auth/external-access-token";
import { readRuntimeEnv } from "@/env";
function trimTrailingSlash(url: string): string {
return url.replace(/\/+$/, "");
}
function readEnv(key: string): string {
const value = import.meta.env[key];
const value = readRuntimeEnv(key);
return typeof value === "string" ? value.trim() : "";
}
+33 -28
View File
@@ -1,28 +1,33 @@
type EnvValue = string | undefined;
function readEnv(
key:
| "VITE_BACKEND_BASE_URL"
| "VITE_LANGGRAPH_BASE_URL"
| "VITE_CANVAS_LANGGRAPH_BASE_URL"
| "VITE_NOTE_API_BASE_URL"
| "VITE_AIPI_BACKENP_URL"
| "VITE_AIPI_FRONTEND_URL"
| "VITE_STATIC_WEBSITE_ONLY"
| "VITE_GITHUB_OAUTH_TOKEN",
): EnvValue {
const value = import.meta.env[key];
return typeof value === "string" && value.trim() ? value : undefined;
}
export const env = {
NEXT_PUBLIC_BACKEND_BASE_URL: readEnv("VITE_BACKEND_BASE_URL"),
NEXT_PUBLIC_LANGGRAPH_BASE_URL: readEnv("VITE_LANGGRAPH_BASE_URL"),
// Canvas page uses its own LangGraph base URL so it can be independently
// routed (e.g. a separate canvas-agent server). Falls back to the main URL.
NEXT_PUBLIC_CANVAS_LANGGRAPH_BASE_URL: readEnv("VITE_CANVAS_LANGGRAPH_BASE_URL"),
NEXT_PUBLIC_NOTE_API_BASE_URL: readEnv("VITE_NOTE_API_BASE_URL"),
NEXT_PUBLIC_NOTEBOOK_API_BASE_URL: readEnv("VITE_AIPI_BACKENP_URL"),
NEXT_PUBLIC_STATIC_WEBSITE_ONLY: readEnv("VITE_STATIC_WEBSITE_ONLY"),
GITHUB_OAUTH_TOKEN: readEnv("VITE_GITHUB_OAUTH_TOKEN"),
};
type EnvValue = string | undefined;
declare global {
interface Window {
__CM_RUNTIME_CONFIG__?: Record<string, string | undefined>;
}
}
export function readRuntimeEnv(key: string): EnvValue {
const runtimeValue =
typeof window !== "undefined" ? window.__CM_RUNTIME_CONFIG__?.[key] : undefined;
if (typeof runtimeValue === "string" && runtimeValue.trim()) {
return runtimeValue.trim();
}
const value = import.meta.env[key];
return typeof value === "string" && value.trim() ? value : undefined;
}
export function readRuntimeFlag(key: string): boolean {
return readRuntimeEnv(key) === "true";
}
export const env = {
NEXT_PUBLIC_BACKEND_BASE_URL: readRuntimeEnv("VITE_BACKEND_BASE_URL"),
NEXT_PUBLIC_LANGGRAPH_BASE_URL: readRuntimeEnv("VITE_LANGGRAPH_BASE_URL"),
// Canvas page uses its own LangGraph base URL so it can be independently
// routed (e.g. a separate canvas-agent server). Falls back to the main URL.
NEXT_PUBLIC_CANVAS_LANGGRAPH_BASE_URL: readRuntimeEnv("VITE_CANVAS_LANGGRAPH_BASE_URL"),
NEXT_PUBLIC_NOTE_API_BASE_URL: readRuntimeEnv("VITE_NOTE_API_BASE_URL"),
NEXT_PUBLIC_NOTEBOOK_API_BASE_URL: readRuntimeEnv("VITE_AIPI_BACKENP_URL"),
NEXT_PUBLIC_STATIC_WEBSITE_ONLY: readRuntimeEnv("VITE_STATIC_WEBSITE_ONLY"),
GITHUB_OAUTH_TOKEN: readRuntimeEnv("VITE_GITHUB_OAUTH_TOKEN"),
};
@@ -14,20 +14,20 @@ import { cn } from '@/lib/utils';
export interface FlowStage {
key: string;
label: string;
emoji: string;
mark: string;
}
/** 固定流水线阶段(key 与后端 ai_writing_job_executor 的 stage key 一致)。 */
export const FLOW_STAGES: readonly FlowStage[] = [
{ key: 'intent', label: '意图理解', emoji: '🧭' },
{ key: 'research', label: '素材搜集', emoji: '🔍' },
{ key: 'material_confirm', label: '素材确认', emoji: '📚' },
{ key: 'outline', label: '大纲规划', emoji: '🗂️' },
{ key: 'outline_confirm', label: '大纲确认', emoji: '🧩' },
{ key: 'draft', label: '写作', emoji: '✍️' },
{ key: 'draft_confirm', label: '草稿确认', emoji: '📝' },
{ key: 'review', label: '编辑审核', emoji: '🧐' },
{ key: 'done', label: '定稿', emoji: '🏁' },
{ key: 'intent', label: '意图理解', mark: '意' },
{ key: 'research', label: '素材搜集', mark: '搜' },
{ key: 'material_confirm', label: '素材确认', mark: '素' },
{ key: 'outline', label: '大纲规划', mark: '纲' },
{ key: 'outline_confirm', label: '大纲确认', mark: '确' },
{ key: 'draft', label: '写作', mark: '写' },
{ key: 'draft_confirm', label: '草稿确认', mark: '稿' },
{ key: 'review', label: '编辑审核', mark: '审' },
{ key: 'done', label: '定稿', mark: '定' },
];
export const FLOW_STAGE_LABEL: Record<string, string> = Object.fromEntries(
@@ -40,12 +40,12 @@ const LINE = 'bg-slate-300 dark:bg-border';
function StageNode({
label,
emoji,
mark,
state,
note,
}: {
label: string;
emoji: string;
mark: string;
state: StageState;
note?: string | null;
}) {
@@ -55,14 +55,14 @@ function StageNode({
{state === 'active' && <span className="absolute inset-0 animate-ping rounded-full bg-primary/25" />}
<span
className={cn(
'relative flex size-11 items-center justify-center rounded-full border-2 bg-white text-lg transition-all dark:bg-card',
'relative flex size-11 items-center justify-center rounded-full border-2 bg-white text-base font-semibold text-primary transition-all dark:bg-card',
state === 'active' && 'border-primary shadow-[0_0_0_3px_rgba(22,119,255,0.12)]',
state === 'done' && 'border-emerald-400/70',
state === 'error' && 'border-destructive',
state === 'pending' && 'border-border opacity-45 grayscale',
)}
>
{emoji}
{mark}
{state === 'done' && (
<span className="absolute -right-0.5 -top-0.5 flex size-4 items-center justify-center rounded-full bg-emerald-500 text-white shadow">
<CheckIcon className="size-2.5" />
@@ -127,7 +127,7 @@ export function AIWritingFlowChart({
{idx > 0 && <div className={cn('mt-[22px] h-px w-5 shrink-0', LINE)} />}
<StageNode
label={stage.label}
emoji={stage.emoji}
mark={stage.mark}
state={stateOf(idx)}
note={stage.key === activeKey ? note : undefined}
/>
@@ -46,6 +46,8 @@ const WRITING_STATUS_PILL: Record<string, { label: string; className: string; sp
error: { label: "出错", className: "text-rose-500" },
};
const SHOW_NOTEBOOK_SPACE_ENTRIES = false;
/** 单条历史记录项 —— 样式参考多智能体会商的 HistoryDropdown 列表项(圆角卡片 + 状态药丸 + 操作按钮)。 */
function WritingHistoryItem({
session,
@@ -83,8 +85,11 @@ function WritingHistoryItem({
return (
<li
className="px-3 py-2.5 transition-colors hover:bg-slate-50"
style={isCurrent ? { backgroundColor: "#ecfdf5" } : undefined}
className={cn(
"px-3 py-2.5 transition-colors hover:bg-slate-50 dark:hover:bg-white/10",
isCurrent &&
"bg-emerald-50 dark:border-l-2 dark:border-[#00e5ff] dark:bg-[#0b3a71]/80 dark:shadow-[inset_0_0_0_1px_rgba(64,150,255,0.24),0_0_18px_rgba(0,180,255,0.14)]",
)}
>
<div className="flex items-start gap-2">
<div className="min-w-0 flex-1">
@@ -101,7 +106,7 @@ function WritingHistoryItem({
}
}}
onBlur={commit}
className="w-full rounded border border-slate-300 px-2 py-1 text-[11.5px] font-semibold text-slate-800 outline-none focus:border-primary"
className="w-full rounded border border-slate-300 px-2 py-1 text-[11.5px] font-semibold text-slate-800 outline-none focus:border-primary dark:border-[#1b477e] dark:bg-[#022248] dark:text-[#e8edf5]"
/>
) : (
<button
@@ -132,7 +137,7 @@ function WritingHistoryItem({
type="button"
onClick={isBackground && onShowProgress ? onShowProgress : onSelect}
title={isBackground ? "查看进度" : "只读查看"}
className="rounded p-1 text-slate-500 transition hover:bg-slate-100 hover:text-primary"
className="rounded p-1 text-slate-500 transition hover:bg-slate-100 hover:text-primary dark:text-[#8aa0c4] dark:hover:bg-white/10 dark:hover:text-[#8fd3ff]"
>
<Eye className="size-3.5" />
</button>
@@ -143,7 +148,7 @@ function WritingHistoryItem({
setRenaming(true);
}}
title="重命名"
className="rounded p-1 text-slate-500 transition hover:bg-slate-100 hover:text-primary"
className="rounded p-1 text-slate-500 transition hover:bg-slate-100 hover:text-primary dark:text-[#8aa0c4] dark:hover:bg-white/10 dark:hover:text-[#8fd3ff]"
>
<Pencil className="size-3.5" />
</button>
@@ -151,7 +156,7 @@ function WritingHistoryItem({
type="button"
onClick={onDelete}
title="删除"
className="rounded p-1 text-slate-500 transition hover:bg-rose-50 hover:text-rose-600"
className="rounded p-1 text-slate-500 transition hover:bg-rose-50 hover:text-rose-600 dark:text-[#8aa0c4] dark:hover:bg-rose-500/15 dark:hover:text-rose-300"
>
<Trash2 className="size-3.5" />
</button>
@@ -248,103 +253,105 @@ export function ResearchSidebar({
</div>
)}
<div className="flex min-h-0 flex-1 flex-col overflow-y-auto overflow-x-hidden px-3 py-4">
<section className="flex flex-col">
<div className="mb-3 flex items-center justify-between px-2">
<h2 className="text-[15px] font-bold tracking-wide text-slate-400">
笔记本
</h2>
<button
type="button"
onClick={() => {
setName("");
setCreateError("");
setCreateOpen(true);
}}
title="新建写作空间"
aria-label="新建写作空间"
className="rounded-md p-1 text-slate-400 transition-colors hover:bg-rose-50 hover:text-rose-600"
>
<Plus className="size-4 stroke-[2.5]" />
</button>
</div>
{SHOW_NOTEBOOK_SPACE_ENTRIES && (
<section className="flex flex-col">
<div className="mb-3 flex items-center justify-between px-2">
<h2 className="text-[15px] font-bold tracking-wide text-slate-400">
笔记本
</h2>
<button
type="button"
onClick={() => {
setName("");
setCreateError("");
setCreateOpen(true);
}}
title="新建写作空间"
aria-label="新建写作空间"
className="rounded-md p-1 text-slate-400 transition-colors hover:bg-rose-50 hover:text-rose-600"
>
<Plus className="size-4 stroke-[2.5]" />
</button>
</div>
<div className="min-h-0 flex-1 space-y-2 overflow-y-auto px-1 pb-3">
{notebooksLoading ? (
<p className="px-3 py-2 text-xs text-slate-400">加载中...</p>
) : notebooksError ? (
<p className="rounded-xl bg-amber-50 px-3 py-2 text-xs leading-5 text-amber-700">
笔记本服务暂不可用,请检查空间服务地址或账号
</p>
) : notebooks.length === 0 ? (
<p className="px-3 py-2 text-xs text-slate-400">暂无笔记本</p>
) : (
notebooks.map((nb) => {
const active = viewingNotebookId === nb.id;
const sourceCount = Math.max(0, nb.source_count ?? 0);
return (
<button
key={nb.id}
type="button"
onClick={() => onOpenNotebook(nb.id)}
title={nb.description || nb.name}
className={cn(
"group relative flex min-h-14 w-full items-center gap-3 rounded-xl border bg-white px-3 py-2 text-left transition-all",
active
? "border-rose-100 bg-rose-50/35 text-slate-900 shadow-[0_6px_18px_rgba(244,63,94,0.06)]"
: "border-slate-100/80 text-slate-600 hover:border-slate-200 hover:bg-slate-50/70",
)}
>
{active && (
<span className="absolute bottom-4 left-0 top-4 w-1 rounded-r-full bg-rose-500" />
)}
<span
<div className="min-h-0 flex-1 space-y-2 overflow-y-auto px-1 pb-3">
{notebooksLoading ? (
<p className="px-3 py-2 text-xs text-slate-400">加载中...</p>
) : notebooksError ? (
<p className="rounded-xl bg-amber-50 px-3 py-2 text-xs leading-5 text-amber-700">
笔记本服务暂不可用,请检查空间服务地址或账号
</p>
) : notebooks.length === 0 ? (
<p className="px-3 py-2 text-xs text-slate-400">暂无笔记本</p>
) : (
notebooks.map((nb) => {
const active = viewingNotebookId === nb.id;
const sourceCount = Math.max(0, nb.source_count ?? 0);
return (
<button
key={nb.id}
type="button"
onClick={() => onOpenNotebook(nb.id)}
title={nb.description || nb.name}
className={cn(
"flex size-8 shrink-0 items-center justify-center rounded-lg",
active ? "bg-rose-50 text-rose-600" : "bg-slate-100 text-slate-500",
"group relative flex min-h-14 w-full items-center gap-3 rounded-xl border bg-white px-3 py-2 text-left transition-all",
active
? "border-rose-100 bg-rose-50/35 text-slate-900 shadow-[0_6px_18px_rgba(244,63,94,0.06)]"
: "border-slate-100/80 text-slate-600 hover:border-slate-200 hover:bg-slate-50/70",
)}
>
<BookOpen className="size-4 stroke-[2.2]" />
</span>
<span className="min-w-0 flex-1">
<span className="block truncate text-[15px] font-medium leading-5">
{nb.name}
</span>
{sourceCount > 0 && (
<span className="mt-1 flex items-center gap-1 text-[11px] text-slate-400">
<FileText className="size-3" />
已有 {sourceCount} 篇排版
</span>
{active && (
<span className="absolute bottom-4 left-0 top-4 w-1 rounded-r-full bg-rose-500" />
)}
</span>
{sourceCount > 0 && (
<span
className={cn(
"flex size-7 shrink-0 items-center justify-center rounded-full border text-[12px] font-semibold",
active
? "border-rose-100 bg-rose-50 text-rose-600"
: "border-slate-100 bg-slate-50 text-slate-400",
"flex size-8 shrink-0 items-center justify-center rounded-lg",
active ? "bg-rose-50 text-rose-600" : "bg-slate-100 text-slate-500",
)}
>
{sourceCount}
<BookOpen className="size-4 stroke-[2.2]" />
</span>
)}
</button>
);
})
)}
</div>
<span className="min-w-0 flex-1">
<span className="block truncate text-[15px] font-medium leading-5">
{nb.name}
</span>
{sourceCount > 0 && (
<span className="mt-1 flex items-center gap-1 text-[11px] text-slate-400">
<FileText className="size-3" />
已有 {sourceCount} 篇排版
</span>
)}
</span>
{sourceCount > 0 && (
<span
className={cn(
"flex size-7 shrink-0 items-center justify-center rounded-full border text-[12px] font-semibold",
active
? "border-rose-100 bg-rose-50 text-rose-600"
: "border-slate-100 bg-slate-50 text-slate-400",
)}
>
{sourceCount}
</span>
)}
</button>
);
})
)}
</div>
<div className="border-t border-slate-100 px-1 pt-3">
<button
type="button"
onClick={onOpenAllSpaces}
className="flex w-full items-center gap-3 rounded-xl px-3 py-3 text-left text-[14px] font-bold text-slate-700 transition-colors hover:bg-slate-50 hover:text-slate-950"
>
<LayoutGrid className="size-4 text-rose-500 stroke-[2.3]" />
<span>所有空间</span>
</button>
</div>
</section>
<div className="border-t border-slate-100 px-1 pt-3">
<button
type="button"
onClick={onOpenAllSpaces}
className="flex w-full items-center gap-3 rounded-xl px-3 py-3 text-left text-[14px] font-bold text-slate-700 transition-colors hover:bg-slate-50 hover:text-slate-950"
>
<LayoutGrid className="size-4 text-rose-500 stroke-[2.3]" />
<span>所有空间</span>
</button>
</div>
</section>
)}
{/* 「历史记录」列表(样式参考多智能体会商):跟随侧栏自然向下滚动、不固定在底部。
点击打开历史回看(显示到右侧区域),✏️/双击改名,🗑删除,「挂起中」点击看进度。 */}
@@ -142,6 +142,7 @@ const DRAFT_ID = 'aw-draft';
const AI_WRITING_AGENT_MODULE_KEY = 'ai-writing';
const AI_WRITING_AGENT_MODULE_TITLE = 'AI 写作智能体';
const AI_WRITING_LAST_AGENT_KEY = `agent-module:${AI_WRITING_AGENT_MODULE_KEY}:last-agent`;
const SHOW_NOTEBOOK_SPACE_ENTRIES = false;
// Match the original canvas page proportions (chat=35, artifact=65)
const DEFAULT_LAYOUT = { [WRITING_ID]: 35, [DRAFT_ID]: 65 };
@@ -567,7 +568,7 @@ function AIWritingBody({
<ArrowLeft className="size-4" />
返回写作
</button>
{rightView.kind === 'notebook' && (
{SHOW_NOTEBOOK_SPACE_ENTRIES && rightView.kind === 'notebook' && (
<button
type="button"
onClick={() => setRightView({ kind: 'allSpaces' })}
+5 -3
View File
@@ -567,9 +567,11 @@ export default function ChatPage() {
isLandingState &&
!compactLanding &&
"-translate-y-[calc(50vh-96px)]",
isLandingState
? "max-w-(--container-width-md)"
: "max-w-(--container-width-lg)",
compactLanding
? "max-w-(--container-width-xl)"
: isLandingState
? "max-w-(--container-width-md)"
: "max-w-(--container-width-lg)",
)}
>
<div className="absolute -top-4 right-0 left-0 z-0">
+1 -1
View File
@@ -613,7 +613,7 @@ export default function NewAgentPage() {
placeholder="写下智能体的人设、边界、工作方式和输出要求"
value={soulInput}
onChange={(event) => setSoulInput(event.currentTarget.value)}
className="max-h-[50vh] min-h-64 resize-y overflow-y-auto font-mono text-xs"
className="max-h-[50vh] min-h-64 resize-y overflow-y-auto text-xs"
/>
</div>
@@ -133,7 +133,7 @@ function DiagnosticRow({ item }: { item: DiagnosticItem }) {
) : null}
{item.userName ? (
<span className="rounded bg-slate-100 px-1.5 py-0.5 text-xs text-slate-600 dark:bg-slate-800 dark:text-slate-300">
👤 {item.userName}
用户 {item.userName}
</span>
) : null}
<span className="ml-auto text-xs text-slate-400 dark:text-muted-foreground">{fmtTime(item.createdAt)}</span>
@@ -31,6 +31,7 @@ import type { SourceItem } from "@/core/studio/types";
import { getStoredAuth } from "@/core/auth";
import { useTheme } from "@/contexts/ThemeContext";
import { useModels } from "@/core/models/hooks";
import { readRuntimeEnv } from "@/env";
import {
Select,
SelectContent,
@@ -259,6 +260,7 @@ export default function StudioNotebookDetailPage({
const notebookId =
notebookIdProp != null ? String(notebookIdProp) : params.notebookId;
const { theme } = useTheme(); // 跳转「我的空间」时带上当前主题 key
const notebookFrontendUrl = readRuntimeEnv("VITE_AIPI_FRONTEND_URL") || "";
const { notebooks, isLoading, token: studioToken } = useStudioNotebooks(); // token 用于跳转 callback
const { models } = useModels();
const queryClient = useQueryClient();
@@ -343,7 +345,7 @@ export default function StudioNotebookDetailPage({
const handleDeleteSource = async (sourceId: number) => {
if (!notebook) return;
try {
const backendUrl = import.meta.env.VITE_AIPI_BACKENP_URL || "http://localhost:8001";
const backendUrl = readRuntimeEnv("VITE_AIPI_BACKENP_URL") || "http://localhost:8001";
const response = await fetch(`${backendUrl}/api/v1/source/${sourceId}`, {
method: "DELETE",
headers: { Authorization: `Bearer ${studioToken ?? ""}` },
@@ -425,7 +427,7 @@ export default function StudioNotebookDetailPage({
const pollSourceStatus = useCallback(async (taskId: string) => {
if (!notebook) return;
try {
const backendUrl = import.meta.env.VITE_AIPI_BACKENP_URL || "http://localhost:8001";
const backendUrl = readRuntimeEnv("VITE_AIPI_BACKENP_URL") || "http://localhost:8001";
const token = getStoredAuth()?.access_token ?? "";
const response = await fetch(`${backendUrl}/api/v1/task/${taskId}/status`, {
headers: { Authorization: `Bearer ${token}` },
@@ -581,7 +583,7 @@ export default function StudioNotebookDetailPage({
</div>
<div className="flex items-center gap-2">
<a
href={`${import.meta.env.VITE_AIPI_FRONTEND_URL}/auth/callback?token=${studioToken ?? ""}&redirect=/dashboard/${notebookId}/new-chat&themekey=${theme}`}
href={`${notebookFrontendUrl}/auth/callback?token=${studioToken ?? ""}&redirect=/dashboard/${notebookId}/new-chat&themekey=${theme}`}
target="_blank"
rel="noreferrer"
className="flex items-center gap-1.5 rounded-full bg-muted px-3 py-1 text-xs text-muted-foreground transition-colors hover:bg-muted/70"
@@ -605,7 +607,7 @@ export default function StudioNotebookDetailPage({
<div className="relative z-10 flex flex-1 flex-col overflow-hidden">
{/* 右上角跳转按钮 */}
<a
href={`${import.meta.env.VITE_AIPI_FRONTEND_URL}/auth/callback?token=${studioToken ?? ""}&redirect=/dashboard/${notebookId}/new-chat&themekey=${theme}`}
href={`${notebookFrontendUrl}/auth/callback?token=${studioToken ?? ""}&redirect=/dashboard/${notebookId}/new-chat&themekey=${theme}`}
target="_blank"
rel="noreferrer"
className="absolute right-6 top-4 z-10 flex items-center gap-1.5 rounded-full bg-muted/70 px-3 py-1.5 text-xs text-muted-foreground backdrop-blur transition-colors hover:bg-muted"
@@ -157,7 +157,7 @@ export function DagFlowChart({
done={consensusState === "done"}
compact={compact}
/>
<MilestoneNode label="共识达成" emoji="🤝" state={consensusState} compact={compact} />
<MilestoneNode label="共识达成" mark="共" state={consensusState} compact={compact} />
{showReportMilestone && (
<>
<Connector
@@ -173,9 +173,9 @@ export function DagFlowChart({
)}
>
<span className="text-[9px] font-bold tracking-widest text-primary/70">并行生成</span>
<MilestoneNode label="总结报告" emoji="📝" state={summaryState} compact={compact} />
<MilestoneNode label="结果绘制" emoji="📊" state={reportState} compact={compact} />
<MilestoneNode label="大屏多页" emoji="🖥️" state={reportState} compact={compact} />
<MilestoneNode label="总结报告" mark="报" state={summaryState} compact={compact} />
<MilestoneNode label="结果绘制" mark="图" state={reportState} compact={compact} />
<MilestoneNode label="大屏多页" mark="屏" state={reportState} compact={compact} />
</div>
</>
)}
@@ -286,12 +286,12 @@ function FlowNode({
function MilestoneNode({
label,
emoji,
mark,
state,
compact,
}: {
label: string;
emoji: string;
mark: string;
state: ChainNodeState;
compact: boolean;
}) {
@@ -306,13 +306,14 @@ function MilestoneNode({
className={cn(
"relative flex items-center justify-center rounded-full border-2 transition-all",
ringSize,
compact ? "text-lg" : "text-2xl",
"font-semibold text-primary",
compact ? "text-base" : "text-xl",
state === "active" && "border-primary bg-primary/5 shadow-[0_0_0_4px_rgba(22,119,255,0.12)]",
state === "done" && "border-emerald-400/70 bg-emerald-500/5",
state === "pending" && "border-border bg-muted opacity-45 grayscale",
)}
>
{emoji}
{mark}
{state === "done" && (
<span className="absolute -right-0.5 -top-0.5 flex size-4 items-center justify-center rounded-full bg-emerald-500 text-white shadow">
<CheckIcon className="size-2.5" />
@@ -106,12 +106,12 @@ function MiniNode({
label,
avatarType,
state,
emoji,
mark,
}: {
label: string;
avatarType?: string;
state: ChainNodeState;
emoji?: string;
mark?: string;
}) {
const tag = avatarType ? chainTagClasses(avatarType) : "";
return (
@@ -123,12 +123,13 @@ function MiniNode({
<span
className={cn(
"relative flex size-11 items-center justify-center rounded-full border-2 bg-white text-lg transition-all dark:bg-card",
mark && "font-semibold text-primary",
state === "active" && "border-primary shadow-[0_0_0_3px_rgba(22,119,255,0.12)]",
state === "done" && "border-emerald-400/70",
state === "pending" && "border-border opacity-45 grayscale",
)}
>
{emoji ?? (avatarType ? getAvatar(avatarType, "h-9 w-9") : null)}
{mark ?? (avatarType ? getAvatar(avatarType, "h-9 w-9") : null)}
{state === "done" && (
<span className="absolute -right-0.5 -top-0.5 flex size-4 items-center justify-center rounded-full bg-emerald-500 text-white shadow">
<CheckIcon className="size-2.5" />
@@ -415,7 +416,7 @@ export function DagFlowMiniPanel({
})}
{/* 共识达成(底) */}
<MiniNode label="共识达成" emoji="🤝" state={consensusState} />
<MiniNode label="共识达成" mark="共" state={consensusState} />
</div>
</div>
@@ -97,7 +97,7 @@ export function DashboardChatPanel({
<div className="shrink-0 flex items-center justify-between gap-2 border-b border-slate-100 bg-white px-4 py-2.5">
<div className="flex items-center gap-2 min-w-0">
<div className="w-6 h-6 shrink-0 rounded-lg bg-gradient-to-tr from-primary to-[var(--brand-200)] flex items-center justify-center text-white text-xs shadow-sm">
📊
图
</div>
<div className="leading-tight min-w-0">
<div className="text-xs font-bold text-slate-800 truncate">大屏绘制智能体</div>
@@ -249,7 +249,7 @@ export function DispatchChainModal({
<Connector active={consensusActive} done={consensusDone} />
<MilestoneNode
label="共识达成"
emoji="🤝"
mark="共"
state={
consensusActive ? "active" : consensusDone ? "done" : "pending"
}
@@ -259,9 +259,9 @@ export function DispatchChainModal({
<Connector active={step3Active} done={step3Done} />
<div className="flex shrink-0 flex-col items-center gap-1.5 rounded-2xl border border-dashed border-primary/30 bg-primary/[0.04] px-2 pb-2 pt-1.5">
<span className="text-[9px] font-bold tracking-widest text-primary/70">并行生成</span>
<MilestoneNode label="总结报告" emoji="📝" state={step3State} />
<MilestoneNode label="结果绘制" emoji="📊" state={step3State} />
<MilestoneNode label="大屏多页" emoji="🖥️" state={step3State} />
<MilestoneNode label="总结报告" mark="报" state={step3State} />
<MilestoneNode label="结果绘制" mark="图" state={step3State} />
<MilestoneNode label="大屏多页" mark="屏" state={step3State} />
</div>
</div>
</div>
@@ -422,11 +422,11 @@ function FlowNode({
function MilestoneNode({
label,
emoji,
mark,
state,
}: {
label: string;
emoji: string;
mark: string;
state: ChainNodeState;
}) {
return (
@@ -437,14 +437,14 @@ function MilestoneNode({
)}
<span
className={cn(
"relative flex size-16 items-center justify-center rounded-full border-2 text-2xl transition-all",
"relative flex size-16 items-center justify-center rounded-full border-2 text-2xl font-semibold text-primary transition-all",
state === "active" &&
"border-primary bg-primary/5 shadow-[0_0_0_4px_rgba(22,119,255,0.12)]",
state === "done" && "border-emerald-400/70 bg-emerald-500/5",
state === "pending" && "border-border bg-muted opacity-45 grayscale",
)}
>
{emoji}
{mark}
{state === "done" && (
<span className="absolute -right-0.5 -top-0.5 flex size-5 items-center justify-center rounded-full bg-emerald-500 text-white shadow">
<CheckIcon className="size-3" />
@@ -705,7 +705,7 @@ export function RecommendAgentsDialog({
{state.phase === "history" && (
<div className="text-xs text-sky-700 dark:text-sky-300 p-2 border border-sky-300/50 dark:border-sky-700/50 rounded-md bg-sky-50 dark:bg-sky-950/30 flex items-center justify-between gap-2">
<span>
📌 这是本会话上次的推荐结果
这是本会话上次的推荐结果
{state.historyCreatedAt
? `(${new Date(state.historyCreatedAt).toLocaleString("zh-CN", { hour12: false })})`
: ""}
@@ -82,9 +82,9 @@ export function RoundtableJobNotifier({ children }: { children: ReactNode }) {
const handleTerminal = useCallback((job: RoundtableJob) => {
setWatchedJobIds((prev) => prev.filter((id) => id !== job.id));
if (job.status === "done") {
setNotice("🎉 后台研讨已完成!可在「历史记录」查看结论与报告");
setNotice("后台研讨已完成!可在「历史记录」查看结论与报告");
} else if (job.status === "error") {
setNotice("❌ 后台任务执行出错,请在「历史记录」中查看");
setNotice("后台任务执行出错,请在「历史记录」中查看");
}
// cancelled:用户主动取消,不提示。
}, []);
@@ -254,7 +254,7 @@ export function Step1Panel(props: Step1PanelProps) {
<div className="flex items-center justify-between border-t border-slate-50 pt-2 text-[#94a3b8]">
<div className="flex items-center gap-1.5">
<button
onClick={() => onTriggerToast("😄 表情包功能仍在微调状态(仅供演示)")}
onClick={() => onTriggerToast("表情包功能仍在微调状态(仅供演示)")}
className="p-1 hover:text-primary hover:bg-slate-50 rounded transition"
>
<Smile className="w-4 h-4" />
@@ -552,7 +552,7 @@ function AgentProgressPanel({
<div className="shrink-0 flex items-center justify-between gap-2 border-b border-slate-100 bg-white px-4 py-2.5">
<div className="flex items-center gap-2 min-w-0">
<div className="w-6 h-6 shrink-0 rounded-lg bg-gradient-to-tr from-primary to-[var(--brand-200)] flex items-center justify-center text-white text-xs shadow-sm">
📊
图
</div>
<div className="leading-tight min-w-0">
<div className="text-xs font-bold text-slate-800 truncate">方案总结智能体</div>
@@ -671,7 +671,7 @@ function AgentProgressPanel({
<div className="shrink-0 flex items-center justify-between gap-2 border-b border-slate-100 bg-white px-4 py-2.5">
<div className="flex items-center gap-2 min-w-0">
<div className="w-6 h-6 shrink-0 rounded-lg bg-gradient-to-tr from-primary to-[var(--brand-200)] flex items-center justify-center text-white text-xs shadow-sm">
📝
报
</div>
<div className="leading-tight min-w-0">
<div className="text-xs font-bold text-slate-800 truncate">方案总结报告智能体</div>
@@ -376,7 +376,7 @@ export function useDraftPersistence(
const id = await enqueueSave(furthest);
if (id) {
await refreshList();
triggerToast("💾 已保存为草稿");
triggerToast("已保存为草稿");
} else {
triggerToast("当前会话还没有可保存的内容");
}
@@ -490,15 +490,15 @@ export function useDraftPersistence(
draft = await apiGet(id);
} catch (err) {
console.warn("[drafts] 加载草稿失败:", err);
triggerToast("⚠️ 加载草稿失败,请重试");
triggerToast("加载草稿失败,请重试");
return false;
}
if (!draft) {
triggerToast("⚠️ 该草稿已不存在");
triggerToast("该草稿已不存在");
await refreshList();
return false;
}
applyLoadedDraft(draft, `📂 已加载草稿:${draft.title}`);
applyLoadedDraft(draft, `已加载草稿:${draft.title}`);
return true;
})();
},
@@ -535,13 +535,13 @@ export function useDraftPersistence(
await apiDelete(id);
} catch (err) {
console.warn("[drafts] 删除草稿失败:", err);
triggerToast("⚠️ 删除失败,请重试");
triggerToast("删除失败,请重试");
return;
}
await refreshList();
if (currentDraftIdRef.current === id) setCurrentDraftId(null);
if (renamingDraftId === id) setRenamingDraftId(null);
triggerToast("🗑 已删除历史记录");
triggerToast("已删除历史记录");
})();
},
[apiDelete, refreshList, renamingDraftId, setCurrentDraftId, triggerToast],
@@ -566,11 +566,11 @@ export function useDraftPersistence(
const updated = await apiUpdate(id, { title: trimmed });
if (updated) {
await refreshList();
triggerToast("✏️ 已重命名");
triggerToast("已重命名");
}
} catch (err) {
console.warn("[drafts] 重命名失败:", err);
triggerToast("⚠️ 重命名失败,请重试");
triggerToast("重命名失败,请重试");
} finally {
setRenamingDraftId(null);
setRenameDraftValue("");
@@ -288,7 +288,7 @@ export function useStep1Intent(options: UseStep1IntentOptions): UseStep1IntentRe
const hasFiles = !!(files && files.length > 0);
if (!trimmed && !hasFiles) return;
if (!intentThreadId) {
triggerToast("⚠️ 任务理解智能体尚未就绪,请稍候");
triggerToast("任务理解智能体尚未就绪,请稍候");
return;
}
// 重入保护用 ref 而不是 state:isIntentSending 是 React state,
@@ -379,7 +379,7 @@ export function useStep1Intent(options: UseStep1IntentOptions): UseStep1IntentRe
: m,
),
);
triggerToast(`❌ 文件上传失败: ${(err as Error).message}`);
triggerToast(`文件上传失败: ${(err as Error).message}`);
if (intentAborterRef.current === aborter) {
intentAborterRef.current = null;
}
@@ -517,7 +517,7 @@ export function useStep1Intent(options: UseStep1IntentOptions): UseStep1IntentRe
: resolvedSummary.objective,
},
]);
triggerToast("✅ 任务理解完成,可进入多智能体圆桌研讨");
triggerToast("任务理解完成,可进入多智能体圆桌研讨");
} else if (res.status === "clarification" && res.clarifications.length > 0) {
setPendingIntentClarificationMsgId(agentMsgId);
setIntentClarificationSelections((prev) => {
@@ -536,7 +536,7 @@ export function useStep1Intent(options: UseStep1IntentOptions): UseStep1IntentRe
setIntentInitError(res.content);
setIntentInitErrorDetail(null);
}
triggerToast(`❌ 智能体调用失败: ${res.content}`);
triggerToast(`智能体调用失败: ${res.content}`);
}
} catch (err) {
if ((err as Error).name === "AbortError" || aborter.signal.aborted) {
@@ -567,7 +567,7 @@ export function useStep1Intent(options: UseStep1IntentOptions): UseStep1IntentRe
: m,
),
);
triggerToast(`❌ 发送失败: ${(err as Error).message}`);
triggerToast(`发送失败: ${(err as Error).message}`);
}
} finally {
if (intentAborterRef.current === aborter) {
@@ -1340,13 +1340,13 @@ export function useStep2Orchestration(
const makeModelSwitchHandler = useCallback(
(bubbleId: number, isParallel: boolean) => (info: ModelSwitchInfo) => {
const who = info.agentName || "智能体";
triggerToast(`⚠️「${info.failedModel}」模型${info.reason},已自动切换「${info.nextModel}」继续`);
triggerToast(`「${info.failedModel}」模型${info.reason},已自动切换「${info.nextModel}」继续`);
if (isParallel) parallelStreamRef.current.delete(bubbleId);
const systemBubble: Step2Dialogue = {
id: Date.now() + Math.random(),
sender: "系统提示",
avatarType: "amber",
content: `⚠️ 模型容错:「${who}」所用模型「${info.failedModel}」${info.reason},已自动切换到「${info.nextModel}」继续。`,
content: `模型容错:「${who}」所用模型「${info.failedModel}」${info.reason},已自动切换到「${info.nextModel}」继续。`,
time: nowTime(),
confidence: "模型容错",
};
@@ -1824,7 +1824,7 @@ export function useStep2Orchestration(
highlightSpeaker(null);
setPendingClarification(leaderRes.clarification);
setClarificationDraft("");
triggerToast("❓ 总控需要补充信息,请回答下方问题后继续");
triggerToast("总控需要补充信息,请回答下方问题后继续");
break;
}
@@ -1843,7 +1843,7 @@ export function useStep2Orchestration(
setConsensusPercentage(100);
setHasConsensus(true);
triggerToast(
"🎉 圆桌研讨论证会圆满落幕!已取得 100% 专家组共识",
"圆桌研讨论证会圆满落幕!已取得 100% 专家组共识",
);
break;
}
@@ -1962,7 +1962,7 @@ export function useStep2Orchestration(
}
} catch (err) {
console.error("orchestration error:", err);
triggerToast(`❌ 研讨异常: ${(err as Error).message}`);
triggerToast(`研讨异常: ${(err as Error).message}`);
} finally {
highlightSpeaker(null);
clearAllStreamingSpeakers();
@@ -2092,7 +2092,7 @@ export function useStep2Orchestration(
highlightSpeaker(null);
setPendingClarification(leaderRes.clarification);
setClarificationDraft("");
triggerToast("❓ 总控需要补充信息,请回答下方问题后继续");
triggerToast("总控需要补充信息,请回答下方问题后继续");
finishedAllSeats = false;
break;
}
@@ -2220,7 +2220,7 @@ export function useStep2Orchestration(
if (closeoutRes.content) setLastLeaderContent(closeoutRes.content);
setConsensusPercentage(100);
setHasConsensus(true);
triggerToast("🎉 业务链条研讨完成!已生成全案结论");
triggerToast("业务链条研讨完成!已生成全案结论");
} catch (err) {
if (closeoutAborter.signal.aborted || (err as Error).name === "AbortError") {
finalizeDialogue(closeoutBubbleId, "(已停止)", "已停止");
@@ -2234,7 +2234,7 @@ export function useStep2Orchestration(
}
} catch (err) {
console.error("chain orchestration error:", err);
triggerToast(`❌ 链条研讨异常: ${(err as Error).message}`);
triggerToast(`链条研讨异常: ${(err as Error).message}`);
} finally {
highlightSpeaker(null);
clearAllStreamingSpeakers();
@@ -2283,7 +2283,7 @@ export function useStep2Orchestration(
async (seedMessage: string, ids: ThreadIdMap, coord: string) => {
const plan = orchestrationPlanRef.current;
if (!plan || plan.stages.length === 0) {
triggerToast("⚠️ 没有可执行的 DAG 编排计划");
triggerToast("没有可执行的 DAG 编排计划");
return;
}
if (isOrchestratingRef.current) return;
@@ -2398,7 +2398,7 @@ export function useStep2Orchestration(
finalizeDialogue(leaderBubbleId, leaderRes.content, "等待用户澄清");
setPendingClarification(leaderRes.clarification);
setClarificationDraft("");
triggerToast("❓ 总控需要补充信息,请回答下方问题后继续");
triggerToast("总控需要补充信息,请回答下方问题后继续");
finishedAllStages = false;
break;
}
@@ -2579,11 +2579,11 @@ export function useStep2Orchestration(
// 不收口(plan.finalSynthesis=false):全部 stage 跑完即视为完成,不再叫总控。
setConsensusPercentage(100);
setHasConsensus(true);
triggerToast("✅ 依赖编排各阶段已全部完成");
triggerToast("依赖编排各阶段已全部完成");
}
} catch (err) {
console.error("dag orchestration error:", err);
triggerToast(`❌ 依赖编排研讨异常: ${(err as Error).message}`);
triggerToast(`依赖编排研讨异常: ${(err as Error).message}`);
} finally {
highlightSpeaker(null);
clearAllStreamingSpeakers();
@@ -2807,10 +2807,10 @@ export function useStep2Orchestration(
appendDialogue({
sender: "系统",
avatarType: "blue",
content: `✅ 圆桌已就绪:协调智能体 ${initRes.main_agent_name} 已上线,${Object.keys(initRes.thread_ids).length - 1} 个席位已分配独立线程。`,
content: `圆桌已就绪:协调智能体 ${initRes.main_agent_name} 已上线,${Object.keys(initRes.thread_ids).length - 1} 个席位已分配独立线程。`,
confidence: "系统就绪",
});
triggerToast("✅ 多智能体圆桌初始化完成");
triggerToast("多智能体圆桌初始化完成");
await runActiveOrchestration(
buildInitialIntent(),
initRes.thread_ids,
@@ -2820,7 +2820,7 @@ export function useStep2Orchestration(
console.error("init multi-agent failed:", err);
if (cancelled) return;
setInitError((err as Error).message);
triggerToast(`❌ 初始化失败: ${(err as Error).message}`);
triggerToast(`初始化失败: ${(err as Error).message}`);
} finally {
if (!cancelled) setIsInitializing(false);
}
@@ -2887,7 +2887,7 @@ export function useStep2Orchestration(
const userText = customAgentInput.trim();
if (!userText) return;
if (!threadIdsRef.current || !coordinatorNameRef.current) {
triggerToast("⚠️ 圆桌尚未就绪,无法记录干预指令");
triggerToast("圆桌尚未就绪,无法记录干预指令");
return;
}
@@ -2895,7 +2895,7 @@ export function useStep2Orchestration(
appendDialogue({
sender: "用户 (插话干预)",
avatarType: "human",
content: `💡 (人工指令介入) ${userText}`,
content: `(人工指令介入) ${userText}`,
confidence: "最高指令",
});
pendingInjectPromptRef.current = `用户人工干预新指令:${userText}\n\n请优先按这条新指令,通过 agent_orchestration 把活派给相关的真实席位去获取交付;若用户点名了某个席位(如"前端实现"),必须派给清单里对应的那个 agent_name。**不要**自己代答,也**不要**在没有真正看到该席位「完成…交付内容为:…」的情况下臆造交付或直接收口。只有当用户明确要求收口、或相关席位都已真实交付时,才综合输出结论。`;
@@ -2908,7 +2908,7 @@ export function useStep2Orchestration(
const hasFiles = !!(files && files.length > 0);
if (!userText && !hasFiles) return;
if (!threadIdsRef.current || !coordinatorNameRef.current) {
triggerToast("⚠️ 圆桌尚未就绪,无法记录干预指令");
triggerToast("圆桌尚未就绪,无法记录干预指令");
return;
}
@@ -2929,7 +2929,7 @@ export function useStep2Orchestration(
status: "uploaded",
}));
} catch (err) {
triggerToast(`❌ 文件上传失败: ${(err as Error).message}`);
triggerToast(`文件上传失败: ${(err as Error).message}`);
return;
}
}
@@ -2947,7 +2947,7 @@ export function useStep2Orchestration(
appendDialogue({
sender: "用户 (插话干预)",
avatarType: "human",
content: `💡 (人工指令介入) ${shownText}${fileNote}`,
content: `(人工指令介入) ${shownText}${fileNote}`,
confidence: "最高指令",
});
@@ -2977,7 +2977,7 @@ export function useStep2Orchestration(
const answer = rawAnswer.trim();
if (!answer) return;
if (!threadIdsRef.current || !coordinatorNameRef.current) {
triggerToast("⚠️ 圆桌尚未就绪,无法回复澄清");
triggerToast("圆桌尚未就绪,无法回复澄清");
return;
}
if (isOrchestratingRef.current) {
@@ -2988,7 +2988,7 @@ export function useStep2Orchestration(
appendDialogue({
sender: "用户 (澄清回复)",
avatarType: "human",
content: `🗣 回复总控提问:${answer}`,
content: `回复总控提问:${answer}`,
confidence: "澄清回复",
});
setPendingClarification(null);
@@ -3008,7 +3008,7 @@ export function useStep2Orchestration(
const handleBudgetChange = useCallback(
(newVal: number) => {
setBudgetLimit(newVal);
triggerToast(`💰 研商经费调优约束变更为 ${newVal} 万!各大智能体启动资源重算...`);
triggerToast(`研商经费调优约束变更为 ${newVal} 万!各大智能体启动资源重算...`);
setTimeout(() => {
const respTime = nowTime();
@@ -3017,13 +3017,13 @@ export function useStep2Orchestration(
let senderName = "风险审查智能体";
if (newVal < 400) {
agentOpinion = `⚠️ [预算警报] 经费压缩至 ${newVal} 万元,自动触发紧急安全妥协防御策略!我们将不得不裁剪旁路冷备份服务器配置,降低静态演练深度,算力容灾等级重下降至 Tier I (低安全水位)。`;
agentOpinion = `[预算警报] 经费压缩至 ${newVal} 万元,自动触发紧急安全妥协防御策略!我们将不得不裁剪旁路冷备份服务器配置,降低静态演练深度,算力容灾等级重下降至 Tier I (低安全水位)。`;
} else if (newVal > 540) {
agentOpinion = `🔋 [方案容量上升] 预算上调至充足的 ${newVal} 万元,资装弹性充裕!我已为 A 套极速构建增加了 4 组外部超大异地区容灾节点支持,灾备冗余度成功突破 Tier III+ 指标,高防高内聚!`;
agentOpinion = `[方案容量上升] 预算上调至充足的 ${newVal} 万元,资装弹性充裕!我已为 A 套极速构建增加了 4 组外部超大异地区容灾节点支持,灾备冗余度成功突破 Tier III+ 指标,高防高内聚!`;
speakerType = "purple";
senderName = "情报收集与适配";
} else {
agentOpinion = `⚡ [资源调配优化] 总体经费设定为 ${newVal} 万元,空间适中。我们重新规划了轻量容器 K3s 集成开销(约 ${Math.round(newVal * 0.45)} 万)和安全审计费比,总体博弈过程保持稳定态收敛。`;
agentOpinion = `[资源调配优化] 总体经费设定为 ${newVal} 万元,空间适中。我们重新规划了轻量容器 K3s 集成开销(约 ${Math.round(newVal * 0.45)} 万)和安全审计费比,总体博弈过程保持稳定态收敛。`;
speakerType = "pink";
senderName = "执行规划智能体";
}
@@ -3059,11 +3059,11 @@ export function useStep2Orchestration(
appendDialogue({
sender: `${roleName} (${targetModel})`,
avatarType: avatarColor,
content: `⚡ [模型偏好已保存] 本席位在下一轮研讨中将使用 ${targetModel} 进行推理。系统提示词与温度(${cfg?.temp ?? 0.2})当前仅在前端记录,等后端开放覆写后再同步生效。`,
content: `[模型偏好已保存] 本席位在下一轮研讨中将使用 ${targetModel} 进行推理。系统提示词与温度(${cfg?.temp ?? 0.2})当前仅在前端记录,等后端开放覆写后再同步生效。`,
confidence: "席位偏好",
});
setSelectedAgentId(null);
triggerToast(`⚙️ "${roleName}"已切换至 ${targetModel},下轮生效`);
triggerToast(`"${roleName}"已切换至 ${targetModel},下轮生效`);
},
[agentConfigs, agentNameToRole, appendDialogue, selectedAgents, selectedModel, triggerToast],
);
@@ -3162,7 +3162,7 @@ export function useStep2Orchestration(
// 与 handleInjectToRoundtable 末尾「直接启动新一轮编排」同构。
const resumeLiveTakeover = useCallback((): boolean => {
if (!threadIdsRef.current || !coordinatorNameRef.current) {
triggerToast("⚠️ 接管失败:未找到研讨线程,已切换为只读查看");
triggerToast("接管失败:未找到研讨线程,已切换为只读查看");
return false;
}
dagPlanExecutedRef.current = true;
@@ -3178,7 +3178,7 @@ export function useStep2Orchestration(
threadIdsRef.current,
coordinatorNameRef.current,
);
triggerToast("🎮 已接管至前端,继续实时研讨");
triggerToast("已接管至前端,继续实时研讨");
return true;
}, [abortAllInFlight, runActiveOrchestration, triggerToast]);
@@ -776,7 +776,7 @@ export function useStep3Report(
setDialogueConfidence(bubbleId, `模型切换:${info.failedModel}→${info.nextModel}`);
appendStepIfNew(
bubbleId,
`⚠️ 模型容错:「${info.failedModel}」${info.reason},改用「${info.nextModel}」`,
`模型容错:「${info.failedModel}」${info.reason},改用「${info.nextModel}」`,
`model_switch:${info.nextModel}`,
);
},
@@ -1097,7 +1097,7 @@ export function useStep3Summary(
setDialogueConfidence(bubbleId, `模型切换:${info.failedModel}→${info.nextModel}`);
appendStepIfNew(
bubbleId,
`⚠️ 模型容错:「${info.failedModel}」${info.reason},改用「${info.nextModel}」`,
`模型容错:「${info.failedModel}」${info.reason},改用「${info.nextModel}」`,
`model_switch:${info.nextModel}`,
);
},
@@ -1228,7 +1228,7 @@ export function useStep3Summary(
setDialogueConfidence(extractBubbleId, `模型切换:${info.failedModel}→${info.nextModel}`);
appendStepIfNew(
extractBubbleId,
`⚠️ 模型容错:「${info.failedModel}」${info.reason},改用「${info.nextModel}」`,
`模型容错:「${info.failedModel}」${info.reason},改用「${info.nextModel}」`,
`model_switch:${info.nextModel}`,
);
},
@@ -1518,7 +1518,7 @@ export function useStep3Summary(
},
});
if (ingestRunRef.current !== myRun || aborter.signal.aborted) return;
finalizeDialogue(bubbleId, res.content || `✅ 已完成「${title}」数据入库。`, "入库完成");
finalizeDialogue(bubbleId, res.content || `已完成「${title}」数据入库。`, "入库完成");
setIngestStatus("done");
persistSummary(); // 把入库气泡 + ingestStatus 落进草稿
} catch (err) {
@@ -406,8 +406,8 @@ export function stripFlowFence(text: string, streaming: boolean): string {
const tail = close >= 0 ? after.slice(close + 3) : "";
const placeholder =
close < 0 && streaming
? "\n\n> 📊 正在输出结构化流程数据,流程图画布同步绘制中…"
: "\n\n> 📊 已生成结构化流程数据,可在右侧「流程图」画布查看(点击节点看详情)。";
? "\n\n> 正在输出结构化流程数据,流程图画布同步绘制中…"
: "\n\n> 已生成结构化流程数据,可在右侧「流程图」画布查看(点击节点看详情)。";
return `${text.slice(0, start).trimEnd()}${placeholder}${tail}`;
}
@@ -138,8 +138,8 @@ export function stripReportFence(text: string, streaming: boolean): string {
// ── 解析(容错:取最后一个 report-json 代码块;缺字段给安全默认) ─────────────────
const FENCE = "```report-json";
const STREAM_MSG = "\n\n> 📊 正在输出大屏结构化数据,右侧沙箱稍后自动渲染…";
const DONE_MSG = "\n\n> 📊 已生成大屏数据,右侧沙箱已渲染预览(可继续输入修改要求调整)。";
const STREAM_MSG = "\n\n> 正在输出大屏结构化数据,右侧沙箱稍后自动渲染…";
const DONE_MSG = "\n\n> 已生成大屏数据,右侧沙箱已渲染预览(可继续输入修改要求调整)。";
/** 文本是否像本契约 JSON(含关键键),避免把无关 {} 误当数据。 */
function looksLikeReportJson(s: string): boolean {
@@ -753,7 +753,7 @@ export function renderDashboardHtml(
const allActive = seatBase ? `${ovBase},${seatBase}` : ovBase;
const navActiveCss = `${allActive}{background:var(--navactive);color:var(--primary);box-shadow:0 1px 5px rgba(15,23,42,.1);font-weight:700;border-color:${P.primary}33}`;
const KPI_ICONS = ["📊", "📈", "⚠️", "🧩", "🏆", "📅"];
const KPI_ICONS = ["数", "趋", "险", "组", "优", "期"];
const kpis = data.kpis
.map((k, i) => {
const ac = P.accents[i % P.accents.length];
@@ -787,7 +787,7 @@ export function renderDashboardHtml(
const console = data.charts.length
? `<section class="console">
<div class="chead"><h2 class="sec">📊 数据研判中心</h2><span class="status">STATUS: REAL-TIME CONSOLE</span></div>
<div class="chead"><h2 class="sec">数据研判中心</h2><span class="status">STATUS: REAL-TIME CONSOLE</span></div>
<div class="charts3">${data.charts.map(chartHtml).join("")}</div>
</section>`
: "";
@@ -799,7 +799,7 @@ export function renderDashboardHtml(
<a class="more" href="#consensus-modal">查看研讨详情 →</a>
</div>
${console}
<h2 class="sec">📋 席位交付总览 <small>点击卡片进入对应席位专页</small></h2>
<h2 class="sec">席位交付总览 <small>点击卡片进入对应席位专页</small></h2>
<div class="seats">${seatCards}</div>
${timeline}
</section>`;
@@ -955,7 +955,7 @@ body:has(.page:target) #overview:not(:target){display:none}
.fill{height:100%;border-radius:99px}
.chips{display:flex;flex-direction:column;gap:6px;margin-top:12px;padding-top:11px;border-top:1px solid var(--line)}
.chip{font-size:10px;padding:3px 8px;border-radius:4px;border-left:2px solid;display:flex;align-items:center;gap:5px}
.chip::before{content:"⚠";font-size:9px}
.chip::before{content:"!";font-size:9px}
.tl{margin-top:2px}
.tl ol{list-style:none;display:grid;grid-template-columns:repeat(auto-fit,minmax(190px,1fr));gap:11px}
.tl li{position:relative;padding:12px 14px;border:1px solid var(--line);border-left:3px solid var(--primary);background:var(--track);border-radius:0 5px 5px 0}
@@ -911,7 +911,7 @@ export default function BusinessChainEditorPage() {
R_CARD,
)}
>
🪶
链
</div>
<div className="leading-tight">
<div className="text-base font-bold text-foreground">业务链条配置</div>
@@ -175,7 +175,7 @@ export default function BusinessChainListPage() {
toast.success(
chain.isPublic
? `已取消发布「${chain.title || "未命名链条"}」`
: `🌍「${chain.title || "未命名链条"}」已发布到公共选用库`,
: `「${chain.title || "未命名链条"}」已发布到公共选用库`,
);
} catch (err) {
toast.error(`操作失败:${err instanceof Error ? err.message : String(err)}`);
@@ -196,7 +196,7 @@ export default function BusinessChainListPage() {
await deleteChain(chain.id);
await refresh(scope);
await loadStats();
toast.success(`🗑️ 已删除链条「${name}」`);
toast.success(`已删除链条「${name}」`);
setPendingDelete(null);
} catch (err) {
toast.error(`删除失败:${err instanceof Error ? err.message : String(err)}`);
@@ -1052,13 +1052,13 @@ export default function RoundtablePlanningPage({
const candidates = filterRecommendCandidates(agents);
const result = buildChainSeatSelection(chain, candidates);
if (!result) {
triggerToast("⚠️ 该业务链条暂无法选用(匹配到的席位不足)");
triggerToast("该业务链条暂无法选用(匹配到的席位不足)");
return;
}
setAutoChainResult(result);
triggerToast(`✅ 已选择业务链「${chain.title}」,完成第一步澄清后点「进入多智能体研讨」`);
triggerToast(`已选择业务链「${chain.title}」,完成第一步澄清后点「进入多智能体研讨」`);
} catch {
triggerToast("❌ 选择业务链失败,请重试");
triggerToast("选择业务链失败,请重试");
}
},
[autoChainResult, triggerToast],
@@ -1276,7 +1276,7 @@ export default function RoundtablePlanningPage({
navigate("/page/strategy/admin/roundtable-diagnostics");
return;
}
triggerToast("⚙️ 系统管理面板拉取中(暂不开放外部审计)");
triggerToast("系统管理面板拉取中(暂不开放外部审计)");
}, [buildWujieConsensusPayload, emitConsensusToParent, inWujieEmbed, navigate, triggerToast]);
// C4(无界回传):第二步**本次 live 研讨** newly 达成共识时,把总控结论 + 各智能体交付广播给父页面。
@@ -1576,7 +1576,7 @@ export default function RoundtablePlanningPage({
const handleStartNewTask = useCallback(() => {
resetToStep1();
triggerToast("✨ 已开始新任务,请在第一步与任务理解智能体澄清需求");
triggerToast("已开始新任务,请在第一步与任务理解智能体澄清需求");
}, [resetToStep1, triggerToast]);
// 深链接(taskId)模式专用的「重新分析」:清空第一步会话、保留当前任务(prefill 不动),并复位
@@ -1585,7 +1585,7 @@ export default function RoundtablePlanningPage({
const handleReanalyze = useCallback(() => {
autoStartedRef.current = false;
resetToStep1();
triggerToast("🔄 已重新分析当前任务");
triggerToast("已重新分析当前任务");
}, [resetToStep1, triggerToast]);
// 无界抽屉保活复用:父页面重开抽屉时 `bus.$emit('roundtable:reset')` → 子应用回到第一步。
@@ -1684,7 +1684,7 @@ export default function RoundtablePlanningPage({
const handleBackgroundSuspend = useCallback(() => {
const selected = step2.selectedAgents;
if (selected.length === 0) {
triggerToast("⚠️ 请先选择研讨席位再后台挂起");
triggerToast("请先选择研讨席位再后台挂起");
return;
}
// —— 跳转前同步冻结所有需要的输入(reset 之后这些 live 状态都会被清空)——
@@ -1706,7 +1706,7 @@ export default function RoundtablePlanningPage({
// 立刻:标记「挂起中」+ 跳回第一步 + 提示(不等接口)。
if (draftId) markPendingSuspend(draftId);
resetToStep1();
triggerToast("🚀 已转入后台,任务会自动跑完研讨、总结报告与结果绘制;可在「历史记录」查看进度");
triggerToast("已转入后台,任务会自动跑完研讨、总结报告与结果绘制;可在「历史记录」查看进度");
// 后台 fire-and-forget:用冻结快照存稿 → 去重 → 建作业 → 注册完成通知。失败只提示、不挡跳转。
void (async () => {
@@ -1742,7 +1742,7 @@ export default function RoundtablePlanningPage({
watchId = job.id;
}
} catch (err) {
triggerToast(`❌ 后台挂起失败:${(err as Error).message}`);
triggerToast(`后台挂起失败:${(err as Error).message}`);
} finally {
// 注册到全工作区通知器,作业完成时**跨页**弹「达成共识同款」提示。
if (watchId) watchJob(watchId);
@@ -1842,7 +1842,7 @@ export default function RoundtablePlanningPage({
id: Date.now() + Math.random(),
sender: "agent",
senderName: "系统提示",
content: "⚠️ 您跳过了当前追问,系统已使用深度预测模型填充默认最稳健配置。",
content: "您跳过了当前追问,系统已使用深度预测模型填充默认最稳健配置。",
time: nowTime(),
});
triggerToast("已跳过,自动填充最优配置");
@@ -1854,7 +1854,7 @@ export default function RoundtablePlanningPage({
sender: "agent",
senderName: "任务理解智能体",
content:
"🔎 为了使方案时间边界更加安全:请问72小时内是否允许团队进行全仿真机房集成演练?这需要额外调拨3% of 预算冗余,但安全系数能提升12%。",
"为了使方案时间边界更加安全:请问72小时内是否允许团队进行全仿真机房集成演练?这需要额外调拨3% of 预算冗余,但安全系数能提升12%。",
time: nowTime(),
});
triggerToast("追问生成就绪!");
@@ -2153,7 +2153,7 @@ export default function RoundtablePlanningPage({
onFollowUp={handleAgentFollowUp}
onEnterRoundtable={() => {
if (!step1.intentReady) {
triggerToast("⚠️ 请先与任务理解智能体完成澄清,生成任务摘要后再进入");
triggerToast("请先与任务理解智能体完成澄清,生成任务摘要后再进入");
return;
}
// 会商自动化串联(模块 C):若任务页带来的业务链已解析成功,跳过选链弹窗,
@@ -2223,12 +2223,12 @@ export default function RoundtablePlanningPage({
onSavePersona={step2.handleSaveAgentAndRecalculate}
onProceedToStep3={() => {
if (!step2.hasConsensus) {
triggerToast("⚠️ 圆桌尚未自然收敛,请等待总控派活全部完成");
triggerToast("圆桌尚未自然收敛,请等待总控派活全部完成");
return;
}
setCurrentStep(3);
triggerToast(
"🎉 多角色智能体博弈共识达成,最终交付全套路线成果文档及方案图!",
"多角色智能体博弈共识达成,最终交付全套路线成果文档及方案图!",
);
}}
onBackgroundSuspend={handleBackgroundSuspend}
@@ -2345,7 +2345,7 @@ function StepNavigation({
locked={step2Locked}
onClick={() => {
if (step2Locked) {
triggerToast("⚠️ 请先在第一步完成任务理解,确认[INTENT_READY]信号后再进入");
triggerToast("请先在第一步完成任务理解,确认[INTENT_READY]信号后再进入");
return;
}
onStepClick(2);
@@ -2361,8 +2361,8 @@ function StepNavigation({
if (step3Locked) {
triggerToast(
intentReady
? "⚠️ 请先在第二步达成共识(总控自然收敛)再进入"
: "⚠️ 请先依次完成第一步和第二步",
? "请先在第二步达成共识(总控自然收敛)再进入"
: "请先依次完成第一步和第二步",
);
return;
}
@@ -2414,7 +2414,7 @@ function StepButton({
{index}
</span>
<span>{label}</span>
{locked && <span className="text-[9px] ml-0.5">🔒</span>}
{locked && <span className="ml-0.5 text-[9px]">锁</span>}
{active && (
<span className="absolute bottom-0 left-1/2 -translate-x-1/2 w-8 h-1 bg-primary rounded-full" />
)}
@@ -4265,7 +4265,7 @@ function RightSidebar({
<span className="text-slate-400 font-medium">共 {timelineEntries.length} 条记录</span>
<button
onClick={() =>
triggerToast(`📊 正在渲染全部 ${timelineEntries.length} 个历史事件归档`)
triggerToast(`正在渲染全部 ${timelineEntries.length} 个历史事件归档`)
}
className="text-primary hover:text-[var(--brand-900)] font-bold hover:underline"
>
@@ -104,6 +104,24 @@
--brand-900: #4096ff; /* 红强调 / 按钮 hover → 蓝(zs-002-05) */
--brand-950: #91caff; /* 深红文字 → 浅蓝(zs-002-03,深底上可读) */
/* Tailwind opacity fallbacks used by Chrome 109. Keep these in sync with
* the scoped semantic tokens above, otherwise /5, /20, etc. become opaque. */
--legacy-background-rgb: 6 14 36;
--legacy-foreground-rgb: 197 207 224;
--legacy-card-rgb: 3 38 80;
--legacy-muted-rgb: 2 24 58;
--legacy-muted-foreground-rgb: 110 136 176;
--legacy-secondary-rgb: 26 47 90;
--legacy-accent-rgb: 26 47 90;
--legacy-primary-rgb: 22 119 255;
--legacy-primary-foreground-rgb: 255 255 255;
--legacy-ring-rgb: 22 119 255;
--legacy-border-rgb: 43 143 254;
--legacy-input-rgb: 90 169 255;
--legacy-destructive-rgb: 245 34 45;
--legacy-workspace-brand-rgb: 22 119 255;
--legacy-brand-200-rgb: 22 51 95;
/* —— 圆桌局部别名(回落到上面已覆盖的 token) —— */
--rt-page-bg: var(--background);
--rt-primary: var(--primary);
@@ -134,22 +152,30 @@
--muted: #0f1c3d;
--secondary: #1a2f5a;
--accent: #1a2f5a;
--legacy-background-rgb: 18 35 71;
--legacy-card-rgb: 12 24 54;
--legacy-muted-rgb: 15 28 61;
--legacy-secondary-rgb: 26 47 90;
--legacy-accent-rgb: 26 47 90;
background-color: var(--background);
background-image: none;
}
.dark .roundtable-planning ::-webkit-scrollbar-thumb,
.roundtable-planning--dark ::-webkit-scrollbar-thumb {
background: rgb(32 132 255 / 70%);
background: color-mix(in oklch, var(--primary) 45%, var(--border));
}
.dark .roundtable-planning ::-webkit-scrollbar-thumb:hover,
.roundtable-planning--dark ::-webkit-scrollbar-thumb:hover {
background: rgb(29 128 255 / 82%);
background: color-mix(in oklch, var(--primary) 65%, var(--border));
}
.dark .roundtable-planning .custom-scrollbar-inner::-webkit-scrollbar-thumb,
.roundtable-planning--dark .custom-scrollbar-inner::-webkit-scrollbar-thumb {
background: rgb(22 119 255 / 35%);
background: color-mix(in oklch, var(--primary) 35%, transparent);
}
@@ -216,15 +242,16 @@
}
:is(.dark .roundtable-planning, .roundtable-planning--dark)
:is(.from-\[var\(--brand-200\)\]) {
--tw-gradient-from: color-mix(in oklch, var(--accent) 60%, transparent) !important;
--tw-gradient-from: rgb(26 47 90 / 60%) !important;
}
:is(.dark .roundtable-planning, .roundtable-planning--dark)
:is(.via-\[var\(--brand-200\)\]) {
--tw-gradient-via: color-mix(in oklch, var(--accent) 60%, transparent) !important;
--tw-gradient-via: rgb(26 47 90 / 60%) !important;
}
/* ---- 品牌浅红底(brand-200 等) → accent ---- */
:is(.dark .roundtable-planning, .roundtable-planning--dark) [class*="brand-200"] {
background-color: rgb(26 47 90 / 70%) !important;
background-color: color-mix(in oklch, var(--accent) 70%, transparent) !important;
}
@@ -323,12 +350,13 @@
.focus-within\:ring-\[var\(--brand-200\)\]\/50:focus-within,
.focus\:ring-\[var\(--brand-200\)\]\/50:focus
) {
--tw-ring-color: color-mix(in oklch, var(--primary) 30%, transparent) !important;
--tw-ring-color: rgb(22 119 255 / 30%) !important;
}
/* ---- 选中边框 border-primary → 主色描边(柔化) ---- */
:is(.dark .roundtable-planning, .roundtable-planning--dark) .border-primary,
:is(.dark .roundtable-planning, .roundtable-planning--dark) .border-2.border-primary {
border-color: rgb(32 132 255 / 70%) !important;
border-color: color-mix(in oklch, var(--primary) 55%, var(--border)) !important;
}
@@ -341,10 +369,12 @@
/* —— 成功 · emerald —— */
:is(.dark .roundtable-planning, .roundtable-planning--dark)
:is(.bg-emerald-50, .bg-emerald-50\/40, .bg-emerald-50\/50, .bg-emerald-100, .bg-emerald-100\/40) {
background-color: #054059 !important;
background-color: color-mix(in oklch, #10b981 18%, var(--card)) !important;
}
:is(.dark .roundtable-planning, .roundtable-planning--dark)
:is(.border-emerald-200, .border-emerald-200\/40, .border-emerald-200\/50) {
border-color: #20a1ca !important;
border-color: color-mix(in oklch, #10b981 42%, var(--border)) !important;
}
:is(.dark .roundtable-planning, .roundtable-planning--dark)
@@ -358,6 +388,7 @@
.bg-amber-50, .bg-amber-50\/60, .bg-amber-100, .bg-amber-100\/40, .bg-amber-200,
.hover\:bg-amber-100:hover, .hover\:bg-amber-200:hover
) {
background-color: #333e42 !important;
background-color: color-mix(in oklch, #f59e0b 20%, var(--card)) !important;
}
:is(.dark .roundtable-planning, .roundtable-planning--dark)
@@ -365,6 +396,7 @@
.border-amber-100, .border-amber-200, .border-amber-200\/40, .border-amber-300,
.border-amber-300\/50, .border-amber-500, .border-amber-700\/50
) {
border-color: #809598 !important;
border-color: color-mix(in oklch, #f59e0b 42%, var(--border)) !important;
}
:is(.dark .roundtable-planning, .roundtable-planning--dark)
@@ -372,12 +404,14 @@
color: #fcd34d !important;
}
:is(.dark .roundtable-planning, .roundtable-planning--dark) .focus\:ring-amber-200:focus {
--tw-ring-color: rgb(245 158 11 / 35%) !important;
--tw-ring-color: color-mix(in oklch, #f59e0b 35%, transparent) !important;
}
/* —— 错误 · red / rose —— */
:is(.dark .roundtable-planning, .roundtable-planning--dark)
:is(.bg-red-50, .bg-red-200, .bg-rose-50, .hover\:bg-rose-50:hover) {
background-color: #2d2b4e !important;
background-color: color-mix(in oklch, #ef4444 18%, var(--card)) !important;
}
:is(.dark .roundtable-planning, .roundtable-planning--dark)
@@ -387,19 +421,23 @@
/* —— 信息 · sky(部分组件已自带 dark: 变体,这里兜底无 dark: 的) —— */
:is(.dark .roundtable-planning, .roundtable-planning--dark) .bg-sky-50 {
background-color: #053d6f !important;
background-color: color-mix(in oklch, #0ea5e9 18%, var(--card)) !important;
}
:is(.dark .roundtable-planning, .roundtable-planning--dark)
:is(.border-sky-200, .border-sky-300\/50) {
border-color: #1f98f5 !important;
border-color: color-mix(in oklch, #0ea5e9 42%, var(--border)) !important;
}
/* —— 装饰 · purple —— */
:is(.dark .roundtable-planning, .roundtable-planning--dark)
:is(.bg-purple-50, .bg-purple-100\/40) {
background-color: #212e6e !important;
background-color: color-mix(in oklch, #a855f7 18%, var(--card)) !important;
}
:is(.dark .roundtable-planning, .roundtable-planning--dark) .border-purple-200 {
border-color: #5f77fb !important;
border-color: color-mix(in oklch, #a855f7 42%, var(--border)) !important;
}
:is(.dark .roundtable-planning, .roundtable-planning--dark)
@@ -409,12 +447,15 @@
/* —— 装饰 · orange / pink —— */
:is(.dark .roundtable-planning, .roundtable-planning--dark) .bg-orange-50 {
background-color: #2f3144 !important;
background-color: color-mix(in oklch, #f97316 18%, var(--card)) !important;
}
:is(.dark .roundtable-planning, .roundtable-planning--dark) .border-orange-200\/50 {
border-color: #828bc7 !important;
border-color: color-mix(in oklch, #f97316 42%, var(--border)) !important;
}
:is(.dark .roundtable-planning, .roundtable-planning--dark) .bg-pink-100\/40 {
background-color: #2e2862 !important;
background-color: color-mix(in oklch, #ec4899 18%, var(--card)) !important;
}
@@ -554,6 +595,7 @@
.dark .custom-scrollbar-inner::-webkit-scrollbar-thumb,
.roundtable-planning--dark .custom-scrollbar-inner::-webkit-scrollbar-thumb {
background: rgb(36 135 254 / 65%);
background: color-mix(in oklch, var(--primary) 35%, var(--border));
}
@@ -10,7 +10,8 @@ import {
ThemeSwitcher,
} from "@/components/workspace/appearance-controls";
import { NotificationDialog } from "@/components/workspace/notification-dialog";
import { getStoredAuth, getAccountDisplayName } from "@/core/auth";
import { getStoredAuth } from "@/core/auth";
import { useLiveAccount } from "@/core/auth/use-live-account";
import { isWujieEnv } from "@/core/embed/wujie";
import { useUnreadCount } from "@/core/notifications";
import { useMyPosition } from "@/core/positions";
@@ -48,13 +49,14 @@ const Header = ({ collapsed }: HeaderProps) => {
const { data: unreadCount = 0 } = useUnreadCount();
const navigate = useNavigate();
const { pathname } = useLocation();
const liveAccount = useLiveAccount();
const auth = getStoredAuth();
const isAdmin = auth?.system_role === "admin";
const visibleMenuItems = useSidebarMenuItems(!!isAdmin);
const { data: myPosition } = useMyPosition();
const positionName = myPosition?.name?.trim() || "";
const displayName = getAccountDisplayName(auth);
const displayName = liveAccount.displayName;
const showTopNav = isDark && !collapsed && !isWujieEnv();
// 浅色(红/蓝)顶栏:一级导航正常横排进顶栏,不做深色那种围绕基座的特殊处理。
// 与 collapsed 解耦(折叠只影响侧边栏,一级导航始终可见);wujie 嵌入下隐藏,
@@ -30,6 +30,7 @@ import {
import { requestCrudService } from '@/service/request-axios-service';
import { authStorageGet } from '@/core/auth/scoped-storage';
import { copyTextToClipboard } from '@/core/clipboard';
import { readRuntimeEnv } from '@/env';
/**
* 通用问答组件
@@ -38,11 +39,11 @@ import { copyTextToClipboard } from '@/core/clipboard';
* @param initialMode - 初始模式(可选)
*/
const zncmUrl = import.meta.env.VITE_API_BASE_URL || '';
const chatUrl = import.meta.env.VITE_AIPI_CHAT_URL || '';
const generateUrl = import.meta.env.VITE_AIPI_GENERATE_CHAT_URL || '';
const taskChatUrl = import.meta.env.VITE_AIPI_TASK_CHAT_URL || '';
const knowledgeUrl = import.meta.env.VITE_AIPI_KNOWLEDGE_URL;
const zncmUrl = readRuntimeEnv('VITE_API_BASE_URL') || '';
const chatUrl = readRuntimeEnv('VITE_AIPI_CHAT_URL') || '';
const generateUrl = readRuntimeEnv('VITE_AIPI_GENERATE_CHAT_URL') || '';
const taskChatUrl = readRuntimeEnv('VITE_AIPI_TASK_CHAT_URL') || '';
const knowledgeUrl = readRuntimeEnv('VITE_AIPI_KNOWLEDGE_URL');
const service = requestCrudService;
const GeneralQA: React.FC<{
onNavigate?: (page: string) => void;
@@ -24,6 +24,7 @@ import { useTheme } from '@/contexts/ThemeContext';
import MarkdownRenderer from '@/strategy-components/MarkdownRenderer';
import { SourceItem } from './GeneralQA.types';
import '../qa/qa-animations.css';
import { readRuntimeEnv } from '@/env';
/**
* 单条检索结果。
@@ -241,7 +242,7 @@ export function ChatMessage<T extends QACommonMessage>({
const isDark = theme === 'dark';
const isUser = message.role === 'user';
const hasTrace = !isUser && !!(message.singleRagTrace || message.thinkingTrace);
const knowledgeUrl = import.meta.env.VITE_AIPI_KNOWLEDGE_URL;
const knowledgeUrl = readRuntimeEnv('VITE_AIPI_KNOWLEDGE_URL');
const [visible, setVisible] = useState(false); // 打开详情对话框
const [listData, setListData] = useState<SourceItem>({
title: '',
@@ -531,9 +532,9 @@ const RetrievalList: React.FC<RetrievalListProps> = ({ items }) => {
title: '',
content: '',
});
const knowledgeUrl = import.meta.env.VITE_AIPI_KNOWLEDGE_URL;
const b1SearchUrl = import.meta.env.VITE_AIPI_B1COH_URL;
const b2SearchUrl = import.meta.env.VITE_AIPI_B2ACTOR_URL;
const knowledgeUrl = readRuntimeEnv('VITE_AIPI_KNOWLEDGE_URL');
const b1SearchUrl = readRuntimeEnv('VITE_AIPI_B1COH_URL');
const b2SearchUrl = readRuntimeEnv('VITE_AIPI_B2ACTOR_URL');
const showDialog = (item: QARetrievalItem) => {
console.log('item', item)
if (item.type === 'more') {
@@ -13,6 +13,8 @@
--yellow: #f59e0b;
--red: #dc2626;
--purple: #7c3aed;
--accent-rgb: 220 38 38;
--text-muted-rgb: 107 114 128;
--button-text: #ffffff;
--surface-hover: rgba(220, 38, 38, 0.04);
--surface-hover-strong: rgba(220, 38, 38, 0.08);
@@ -55,6 +57,8 @@
--yellow: #f59e0b;
--red: #f43f5e;
--purple: #8b5cf6;
--accent-rgb: 59 130 246;
--text-muted-rgb: 148 163 184;
--button-text: #ffffff;
--surface-hover: rgba(255, 255, 255, 0.03);
--surface-hover-strong: rgba(255, 255, 255, 0.06);
@@ -137,6 +141,7 @@
}
.mope-chat .app-header {
background: var(--surface);
background: color-mix(in srgb, var(--surface) 92%, var(--accent) 8%);
border-bottom: 1px solid var(--border);
padding: 14px 18px;
@@ -179,6 +184,7 @@
flex-shrink: 0;
overflow-y: auto;
border-right: 1px solid var(--border);
background: var(--surface);
background: color-mix(in srgb, var(--surface) 96%, var(--accent) 4%);
}
@@ -194,6 +200,7 @@
flex-shrink: 0;
overflow-y: auto;
border-left: 1px solid var(--border);
background: var(--surface);
background: color-mix(in srgb, var(--surface) 96%, var(--accent) 4%);
}
@@ -322,11 +329,11 @@
letter-spacing: 0.3px;
}
.mope-chat .badge-starting { background: color-mix(in srgb, var(--accent) 15%, transparent); color: var(--accent); }
.mope-chat .badge-diverging { background: color-mix(in srgb, var(--yellow) 15%, transparent); color: var(--yellow); }
.mope-chat .badge-closing { background: color-mix(in srgb, var(--green) 15%, transparent); color: var(--green); }
.mope-chat .badge-done { background: color-mix(in srgb, var(--purple) 15%, transparent); color: var(--purple); }
.mope-chat .badge-default { background: color-mix(in srgb, var(--text-muted) 15%, transparent); color: var(--text-muted); }
.mope-chat .badge-starting { background: rgb(var(--accent-rgb) / 15%); background: color-mix(in srgb, var(--accent) 15%, transparent); color: var(--accent); }
.mope-chat .badge-diverging { background: rgb(245 158 11 / 15%); background: color-mix(in srgb, var(--yellow) 15%, transparent); color: var(--yellow); }
.mope-chat .badge-closing { background: rgb(16 185 129 / 15%); background: color-mix(in srgb, var(--green) 15%, transparent); color: var(--green); }
.mope-chat .badge-done { background: rgb(139 92 246 / 15%); background: color-mix(in srgb, var(--purple) 15%, transparent); color: var(--purple); }
.mope-chat .badge-default { background: rgb(var(--text-muted-rgb) / 15%); background: color-mix(in srgb, var(--text-muted) 15%, transparent); color: var(--text-muted); }
/* Spinner */
@keyframes spin {
@@ -133,6 +133,7 @@
.config-mode-switch {
display: inline-flex;
background: var(--surface);
background: color-mix(in srgb, var(--surface) 96%, var(--accent) 4%);
border: 1px solid var(--border);
border-radius: 12px;
@@ -281,6 +282,7 @@
.config-textarea::placeholder,
.config-input::placeholder,
.config-json-editor::placeholder {
color: rgb(var(--text-muted-rgb) / 72%);
color: color-mix(in srgb, var(--text-muted) 72%, transparent);
}
@@ -389,7 +389,9 @@
margin-top: 10px;
margin-bottom: 0;
padding: 10px 12px;
border: 1px solid color-mix(in srgb, var(--accent) 28%, transparent);
border: 1px solid rgb(var(--accent-rgb) / 28%);
border-color: color-mix(in srgb, var(--accent) 28%, transparent);
box-shadow: 0 8px 18px rgb(var(--accent-rgb) / 10%);
box-shadow: 0 8px 18px color-mix(in srgb, var(--accent) 10%, transparent);
}
@@ -8,6 +8,7 @@ import { Col, Loading, Pagination, Progress, Row } from 'tdesign-react';
import { requestCrudService } from '@/service/request-axios-service';
import { ActionColumn } from './ActionColumn';
import { resolveStatusIcon } from './icons';
import { readRuntimeEnv } from '@/env';
interface ActionAnalyzesProps {
TaskType?: string;
@@ -17,7 +18,7 @@ interface ActionAnalyzesProps {
}
const service = requestCrudService;
const zncmUrl = import.meta.env.VITE_API_BASE_URL || '';
const zncmUrl = readRuntimeEnv('VITE_API_BASE_URL') || '';
export const ActionAnalyzes: React.FC<ActionAnalyzesProps> = ({ title, category, subCategory}) => {
const { theme } = useTheme();
const isDark = theme === 'dark';
@@ -7,6 +7,7 @@ import { useNavigate } from 'react-router-dom';
import { Col, Pagination, Progress, Row } from 'tdesign-react';
import { requestCrudService } from '@/service/request-axios-service';
import { resolveStatusIcon } from './icons';
import { readRuntimeEnv } from '@/env';
interface ActionColumnProps {
TaskType?: string;
@@ -21,7 +22,7 @@ interface ActionColumnProps {
}
const service = requestCrudService;
const zncmUrl = import.meta.env.VITE_API_BASE_URL || '';
const zncmUrl = readRuntimeEnv('VITE_API_BASE_URL') || '';
export const ActionColumn: React.FC<ActionColumnProps> = ({ title, category, subCategory, icon, columnName, columnIndex, actionCount, actionList }) => {
const { theme } = useTheme();
const isDark = theme === 'dark';
@@ -9,6 +9,7 @@ import { toast } from 'sonner';
import { KanbanColumn } from './KanbanColumn';
import { TaskListItem } from './TaskManagement';
import { listBusinessMappings } from '@/strategy-components/api/business-mapping';
import { readRuntimeEnv } from '@/env';
interface TaskListPageProps {
title: string;
@@ -25,7 +26,7 @@ interface StateItem {
type ColumnType = 'unanalyzed' | 'running' | 'completed';
const service = requestCrudService;
const zncmUrl = import.meta.env.VITE_API_BASE_URL || '';
const zncmUrl = readRuntimeEnv('VITE_API_BASE_URL') || '';
/** 本页对应的业务码(3QFX = 3Q);用于查业务映射拿到要自动选用的业务链 / 单智能体。 */
const BUSINESS_CODE = '3Q';
@@ -9,6 +9,7 @@ import { toast } from 'sonner';
import { KanbanColumn } from './KanbanColumn';
import { TaskListItem } from './TaskManagement';
import { listBusinessMappings } from '@/strategy-components/api/business-mapping';
import { readRuntimeEnv } from '@/env';
interface TaskListPageProps {
title: string;
@@ -25,7 +26,7 @@ interface StateItem {
type ColumnType = 'unanalyzed' | 'running' | 'completed';
const service = requestCrudService;
const zncmUrl = import.meta.env.VITE_API_BASE_URL || '';
const zncmUrl = readRuntimeEnv('VITE_API_BASE_URL') || '';
/** 本页对应的业务码(任务FX = 6BF);用于查业务映射拿到要自动选用的业务链 / 单智能体。 */
const BUSINESS_CODE = '6BF';
+216 -80
View File
@@ -135,23 +135,6 @@
}
}
--animate-wave: wave 0.6s ease-in-out 2;
@keyframes wave {
0%,
100% {
transform: rotate(0deg);
}
25% {
transform: rotate(20deg);
}
50% {
transform: rotate(0deg);
}
75% {
transform: rotate(20deg);
}
}
--animate-breathe-stop: breathe-stop 1.8s ease-in-out infinite;
@keyframes breathe-stop {
0%,
@@ -253,73 +236,113 @@
--brand-900: #cf1322;
--brand-950: #a8071a;
--radius: 0.75rem;
--background: oklch(1 0 0);
--foreground: oklch(0.22 0.02 25);
--card: oklch(1 0.002 95);
--card-foreground: oklch(0.22 0.02 25);
--popover: oklch(1 0.002 95);
--popover-foreground: oklch(0.22 0.02 25);
--primary: oklch(0.52 0.19 27);
--primary-foreground: oklch(0.99 0 0);
--secondary: oklch(0.955 0.008 95);
--secondary-foreground: oklch(0.32 0.04 25);
--muted: oklch(0.965 0.006 95);
--muted-foreground: oklch(0.48 0.02 25);
--accent: oklch(0.96 0.04 25);
--accent-foreground: oklch(0.35 0.12 25);
--destructive: oklch(0.577 0.245 27.325);
--border: oklch(0.9 0.008 95);
--input: oklch(0.9 0.01 95);
--ring: oklch(0.52 0.19 27 / 0.35);
--chart-1: oklch(0.646 0.222 41.116);
--chart-2: oklch(0.6 0.118 184.704);
--chart-3: oklch(0.398 0.07 227.392);
--chart-4: oklch(0.828 0.189 84.429);
--chart-5: oklch(0.769 0.188 70.08);
--sidebar: oklch(0.995 0.003 95);
--sidebar-foreground: oklch(0.28 0.02 25);
--sidebar-primary: oklch(0.52 0.19 27);
--sidebar-primary-foreground: oklch(0.99 0 0);
--sidebar-accent: oklch(0.978 0.018 70);
--sidebar-accent-foreground: oklch(0.32 0.05 32);
--sidebar-border: oklch(0.92 0.01 85);
--sidebar-ring: oklch(0.52 0.19 27 / 0.4);
/* Keep the base design tokens in legacy color syntax. CSS custom
properties containing oklch() make every dependent Tailwind color rule
invalid in Chrome 109 and below. */
--background: #ffffff;
--foreground: #231716;
--card: #ffffff;
--card-foreground: #231716;
--popover: #ffffff;
--popover-foreground: #231716;
--primary: #be2323;
--primary-foreground: #fcfcfc;
--secondary: #f2f0ea;
--secondary-foreground: #513b38;
--muted: #f5f3ef;
--muted-foreground: #695958;
--accent: #ffedeb;
--accent-foreground: #7b2727;
--destructive: #e40014;
--border: #dfded8;
--input: #e0ded7;
--ring: rgba(190, 35, 35, 0.35);
--chart-1: #d94a32;
--chart-2: #188f95;
--chart-3: #4f6680;
--chart-4: #e8a72a;
--chart-5: #db8d16;
--sidebar: #fdfcfb;
--sidebar-foreground: #322524;
--sidebar-primary: #be2323;
--sidebar-primary-foreground: #fcfcfc;
--sidebar-accent: #fff6ec;
--sidebar-accent-foreground: #633a32;
--sidebar-border: #e8e4dd;
--sidebar-ring: rgba(190, 35, 35, 0.4);
/* Chrome 109 fallback channels for Tailwind opacity modifiers. */
--legacy-accent-rgb: 255 237 235;
--legacy-background-rgb: 255 255 255;
--legacy-border-rgb: 223 222 216;
--legacy-brand-200-rgb: 254 237 237;
--legacy-card-rgb: 255 255 255;
--legacy-destructive-rgb: 228 0 20;
--legacy-foreground-rgb: 35 23 22;
--legacy-input-rgb: 224 222 215;
--legacy-muted-rgb: 245 243 239;
--legacy-muted-foreground-rgb: 105 89 88;
--legacy-primary-rgb: 190 35 35;
--legacy-primary-foreground-rgb: 252 252 252;
--legacy-ring-rgb: 190 35 35;
--legacy-secondary-rgb: 242 240 234;
--legacy-sidebar-accent-rgb: 255 246 236;
--legacy-sidebar-border-rgb: 232 228 221;
--legacy-sidebar-foreground-rgb: 50 37 36;
--legacy-workspace-brand-rgb: 245 34 45;
}
.dark {
--workspace-brand: #3b82f6;
--workspace-brand-foreground: #ffffff;
--background: oklch(0.208 0.042 264);
--foreground: oklch(0.96 0.005 240);
--card: oklch(0.265 0.04 260);
--card-foreground: oklch(0.96 0.005 240);
--popover: oklch(0.245 0.04 262);
--popover-foreground: oklch(0.96 0.005 240);
--primary: oklch(0.623 0.214 259);
--primary-foreground: oklch(0.99 0 0);
--secondary: oklch(0.32 0.038 260);
--secondary-foreground: oklch(0.96 0.005 240);
--muted: oklch(0.30 0.035 260);
--muted-foreground: oklch(0.68 0.02 240);
--accent: oklch(0.35 0.06 260);
--accent-foreground: oklch(0.96 0.005 240);
--destructive: oklch(0.704 0.191 22.216);
--border: oklch(1 0 0 / 12%);
--input: oklch(1 0 0 / 14%);
--ring: oklch(0.623 0.214 259 / 0.45);
--chart-1: oklch(0.488 0.243 264.376);
--chart-2: oklch(0.696 0.17 162.48);
--chart-3: oklch(0.769 0.188 70.08);
--chart-4: oklch(0.627 0.265 303.9);
--chart-5: oklch(0.645 0.246 16.439);
--sidebar: oklch(0.185 0.04 265);
--sidebar-foreground: oklch(0.94 0.005 240);
--sidebar-primary: oklch(0.623 0.214 259);
--sidebar-primary-foreground: oklch(0.99 0 0);
--sidebar-accent: oklch(0.30 0.055 260);
--sidebar-accent-foreground: oklch(0.96 0.005 240);
--sidebar-border: oklch(1 0 0 / 10%);
--sidebar-ring: oklch(0.623 0.214 259 / 0.45);
--background: #0e172b;
--foreground: #eff2f5;
--card: #1a2539;
--card-foreground: #eff2f5;
--popover: #17243a;
--popover-foreground: #eff2f5;
--primary: #2b81ff;
--primary-foreground: #fcfcfc;
--secondary: #273346;
--secondary-foreground: #eff2f5;
--muted: #232e40;
--muted-foreground: #8e9aa4;
--accent: #283b5a;
--accent-foreground: #eff2f5;
--destructive: #ff6568;
--border: rgba(255, 255, 255, 0.12);
--input: rgba(255, 255, 255, 0.14);
--ring: rgba(43, 129, 255, 0.45);
--chart-1: #236bdb;
--chart-2: #51c49d;
--chart-3: #e8b02a;
--chart-4: #a65be5;
--chart-5: #e65a42;
--sidebar: #0d1830;
--sidebar-foreground: #e8ecee;
--sidebar-primary: #2b81ff;
--sidebar-primary-foreground: #fcfcfc;
--sidebar-accent: #1d2e49;
--sidebar-accent-foreground: #eff2f5;
--sidebar-border: rgba(255, 255, 255, 0.1);
--sidebar-ring: rgba(43, 129, 255, 0.45);
--legacy-accent-rgb: 40 59 90;
--legacy-background-rgb: 14 23 43;
--legacy-border-rgb: 255 255 255;
--legacy-card-rgb: 26 37 57;
--legacy-destructive-rgb: 255 101 104;
--legacy-foreground-rgb: 239 242 245;
--legacy-input-rgb: 255 255 255;
--legacy-muted-rgb: 35 46 64;
--legacy-muted-foreground-rgb: 142 154 164;
--legacy-primary-rgb: 43 129 255;
--legacy-primary-foreground-rgb: 252 252 252;
--legacy-ring-rgb: 43 129 255;
--legacy-secondary-rgb: 39 51 70;
--legacy-sidebar-accent-rgb: 29 46 73;
--legacy-sidebar-border-rgb: 255 255 255;
--legacy-sidebar-foreground-rgb: 232 236 238;
--legacy-workspace-brand-rgb: 59 130 246;
font-weight: 300;
}
@@ -364,6 +387,24 @@
--sidebar-accent-foreground: #0056c6;
--sidebar-border: #e6ebf2;
--sidebar-ring: rgb(0 98 217 / 0.4);
--legacy-accent-rgb: 234 244 255;
--legacy-background-rgb: 255 255 255;
--legacy-border-rgb: 219 219 219;
--legacy-brand-200-rgb: 234 244 255;
--legacy-card-rgb: 255 255 255;
--legacy-destructive-rgb: 245 34 45;
--legacy-foreground-rgb: 61 61 61;
--legacy-input-rgb: 219 219 219;
--legacy-muted-rgb: 243 245 248;
--legacy-muted-foreground-rgb: 127 127 127;
--legacy-primary-rgb: 0 98 217;
--legacy-primary-foreground-rgb: 255 255 255;
--legacy-ring-rgb: 0 98 217;
--legacy-secondary-rgb: 244 249 255;
--legacy-sidebar-accent-rgb: 234 244 255;
--legacy-sidebar-border-rgb: 230 235 242;
--legacy-sidebar-foreground-rgb: 61 61 61;
--legacy-workspace-brand-rgb: 0 98 217;
}
@layer base {
@@ -698,7 +739,7 @@
}
.dark .workspace-panel {
border: 1px solid oklch(1 0 0 / 10%);
border: 1px solid rgba(255, 255, 255, 0.1);
background: var(--card);
box-shadow: none;
}
@@ -1004,6 +1045,7 @@
--container-width-sm: calc(var(--spacing) * 144);
--container-width-md: calc(var(--spacing) * 204);
--container-width-lg: calc(var(--spacing) * 256);
--container-width-xl: calc(var(--spacing) * 280);
}
/* ── Landing 态输入框 Gemini 风格:旋转彩色描边 + 多色柔光阴影 ───────── */
@@ -1237,3 +1279,97 @@ html.lightblue .tiptap-writing-editor .ProseMirror ::selection {
color: #1890ff;
z-index: 2;
}
/* --------------------------------------------------------------------------
* Legacy Chrome visual fallbacks
*
* Tailwind v4 deliberately uses color-mix() for `bg-primary/10` and similar
* opacity utilities. In Chrome versions without color-mix(), its generated
* fallback is the fully opaque primary color. That turns pale selected cards
* into solid red blocks and makes same-color icons disappear. Keep critical
* agent surfaces on classic rgba()/hex colours instead.
* -------------------------------------------------------------------------- */
.agent-avatar {
background-color: #feeded;
color: #cf1322;
box-shadow: inset 0 0 0 1px rgba(190, 35, 35, 0.15);
}
.dark .agent-avatar {
background-color: #1b355d;
color: #ffffff;
box-shadow: inset 0 0 0 1px rgba(91, 157, 255, 0.38);
}
.lightblue .agent-avatar {
background-color: #eaf4ff;
color: #0062d9;
box-shadow: inset 0 0 0 1px rgba(0, 98, 217, 0.16);
}
.agent-sidebar-card--active {
background-color: #feeded;
border-color: rgba(190, 35, 35, 0.5);
}
.dark .agent-sidebar-card--active {
background-color: #0b4fae;
border-color: #2b8ffe;
}
.lightblue .agent-sidebar-card--active {
background-color: #eaf4ff;
border-color: rgba(0, 98, 217, 0.5);
}
@supports not (color: oklch(1 0 0)) {
.bg-primary\/5 { background-color: rgba(190, 35, 35, 0.05); }
.bg-primary\/8 { background-color: rgba(190, 35, 35, 0.08); }
.bg-primary\/10 { background-color: rgba(190, 35, 35, 0.10); }
.bg-primary\/14 { background-color: rgba(190, 35, 35, 0.14); }
.bg-primary\/15 { background-color: rgba(190, 35, 35, 0.15); }
.bg-primary\/20 { background-color: rgba(190, 35, 35, 0.20); }
.bg-primary\/25 { background-color: rgba(190, 35, 35, 0.25); }
.bg-primary\/30 { background-color: rgba(190, 35, 35, 0.30); }
.bg-primary\/60 { background-color: rgba(190, 35, 35, 0.60); }
.bg-primary\/70 { background-color: rgba(190, 35, 35, 0.70); }
.bg-primary\/90 { background-color: rgba(190, 35, 35, 0.90); }
.border-primary\/15 { border-color: rgba(190, 35, 35, 0.15); }
.border-primary\/20 { border-color: rgba(190, 35, 35, 0.20); }
.border-primary\/25 { border-color: rgba(190, 35, 35, 0.25); }
.border-primary\/30 { border-color: rgba(190, 35, 35, 0.30); }
.border-primary\/35 { border-color: rgba(190, 35, 35, 0.35); }
.border-primary\/40 { border-color: rgba(190, 35, 35, 0.40); }
.border-primary\/50 { border-color: rgba(190, 35, 35, 0.50); }
.border-primary\/60 { border-color: rgba(190, 35, 35, 0.60); }
.ring-primary\/15 { --tw-ring-color: rgba(190, 35, 35, 0.15); }
.ring-primary\/20 { --tw-ring-color: rgba(190, 35, 35, 0.20); }
.ring-primary\/30 { --tw-ring-color: rgba(190, 35, 35, 0.30); }
.ring-primary\/40 { --tw-ring-color: rgba(190, 35, 35, 0.40); }
.ring-primary\/50 { --tw-ring-color: rgba(190, 35, 35, 0.50); }
.ring-primary\/60 { --tw-ring-color: rgba(190, 35, 35, 0.60); }
.dark .bg-primary\/5 { background-color: rgba(43, 129, 255, 0.05); }
.dark .bg-primary\/8 { background-color: rgba(43, 129, 255, 0.08); }
.dark .bg-primary\/10 { background-color: rgba(43, 129, 255, 0.10); }
.dark .bg-primary\/14 { background-color: rgba(43, 129, 255, 0.14); }
.dark .bg-primary\/15 { background-color: rgba(43, 129, 255, 0.15); }
.dark .bg-primary\/20 { background-color: rgba(43, 129, 255, 0.20); }
.dark .bg-primary\/25 { background-color: rgba(43, 129, 255, 0.25); }
.dark .bg-primary\/30 { background-color: rgba(43, 129, 255, 0.30); }
.dark .bg-primary\/60 { background-color: rgba(43, 129, 255, 0.60); }
.dark .bg-primary\/70 { background-color: rgba(43, 129, 255, 0.70); }
.dark .bg-primary\/90 { background-color: rgba(43, 129, 255, 0.90); }
.lightblue .bg-primary\/5 { background-color: rgba(0, 98, 217, 0.05); }
.lightblue .bg-primary\/8 { background-color: rgba(0, 98, 217, 0.08); }
.lightblue .bg-primary\/10 { background-color: rgba(0, 98, 217, 0.10); }
.lightblue .bg-primary\/14 { background-color: rgba(0, 98, 217, 0.14); }
.lightblue .bg-primary\/15 { background-color: rgba(0, 98, 217, 0.15); }
.lightblue .bg-primary\/20 { background-color: rgba(0, 98, 217, 0.20); }
.lightblue .bg-primary\/25 { background-color: rgba(0, 98, 217, 0.25); }
.lightblue .bg-primary\/30 { background-color: rgba(0, 98, 217, 0.30); }
.lightblue .bg-primary\/60 { background-color: rgba(0, 98, 217, 0.60); }
.lightblue .bg-primary\/70 { background-color: rgba(0, 98, 217, 0.70); }
.lightblue .bg-primary\/90 { background-color: rgba(0, 98, 217, 0.90); }
}
+4 -3
View File
@@ -9,22 +9,23 @@ import {
matchSqReportMock,
sqReportMockAdapter,
} from '@/strategy-components/components/task-components/sq-report-mock-data'
import { readRuntimeFlag } from '@/env'
// 任务看板(任务分析 / 三情分析)数据源开关:
// VITE_USE_REAL_DATA=true → 走真实接口;其它(含未设置)→ 这两个接口走本地假数据。
// 提交的 .env.development/.env.production/.env.network/.env.sit 默认 true(真实数据),
// 仅本机 .env 默认 false(假数据)。改 env 后需重启 dev server 生效。
const USE_REAL_DATA = import.meta.env.VITE_USE_REAL_DATA === 'true'
const USE_REAL_DATA = readRuntimeFlag('VITE_USE_REAL_DATA')
// 任务详情(cop-task-detail,登录深链用)数据源开关:
// 默认真实数据;仅当 VITE_MOCK_COP_TASK_DETAIL=true 时走本地假数据(consumer 外网不可达时调试用)。
// 与 USE_REAL_DATA 独立。改 env 后需重启 dev server 生效。
const MOCK_COP_TASK_DETAIL = import.meta.env.VITE_MOCK_COP_TASK_DETAIL === 'true'
const MOCK_COP_TASK_DETAIL = readRuntimeFlag('VITE_MOCK_COP_TASK_DETAIL')
// 研判情报报告(selectSqReport,大屏绘制智能体深链用)数据源开关:
// 默认真实数据;仅当 VITE_MOCK_SQ_REPORT=true 时走本地假数据(真实接口未就绪时调试用)。
// 与上面两个开关独立。改 env 后需重启 dev server 生效。
const MOCK_SQ_REPORT = import.meta.env.VITE_MOCK_SQ_REPORT === 'true'
const MOCK_SQ_REPORT = readRuntimeFlag('VITE_MOCK_SQ_REPORT')
// axios.interceptors
const service = axios.create({
+20 -4
View File
@@ -5,10 +5,22 @@ import react from "@vitejs/plugin-react";
import { defineConfig } from "vite";
import sourceIdentifierPlugin from "vite-plugin-source-identifier";
const rootDir = fileURLToPath(new URL(".", import.meta.url));
const rootDir = fileURLToPath(new URL(".", import.meta.url));
// Tailwind 4 emits CSS cascade layers, which require Chrome 99+. Chrome 100
// is therefore the practical lower bound for this Win7 legacy build.
const LEGACY_CHROME = 100 << 16;
export default defineConfig({
plugins: [sourceIdentifierPlugin(), react()],
export default defineConfig({
plugins: [sourceIdentifierPlugin(), react()],
css: {
// Also used by the production CSS minifier. Development CSS is handled
// by the matching PostCSS compatibility pass in postcss.config.js.
lightningcss: {
targets: {
chrome: LEGACY_CHROME,
},
},
},
resolve: {
alias: [
{
@@ -33,7 +45,11 @@ export default defineConfig({
},
],
},
"base":"./",
"base":"./",
build: {
cssMinify: "lightningcss",
target: "chrome100",
},
server: {
// 与 scripts/env.sh FRONTEND_PORT=5174、圆桌 wujie 嵌入文档一致。
port: 5173,
-311
View File
@@ -1,311 +0,0 @@
{
"name": "deerflow-server",
"lockfileVersion": 3,
"requires": true,
"packages": {
"node_modules/agent-base": {
"version": "6.0.2",
"resolved": "https://registry.npmmirror.com/agent-base/-/agent-base-6.0.2.tgz",
"integrity": "sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==",
"dependencies": {
"debug": "4"
},
"engines": {
"node": ">= 6.0.0"
}
},
"node_modules/asynckit": {
"version": "0.4.0",
"resolved": "https://registry.npmmirror.com/asynckit/-/asynckit-0.4.0.tgz",
"integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q=="
},
"node_modules/axios": {
"version": "1.16.1",
"resolved": "https://registry.npmmirror.com/axios/-/axios-1.16.1.tgz",
"integrity": "sha512-caYkukvroVPO8KrzuJEb50Hm07KwfBZPEC3VeFHTsqWHvKTsy54hjJz9BS/cdaypROE2rH6xvm9mHX4fgWkr3A==",
"dependencies": {
"follow-redirects": "^1.16.0",
"form-data": "^4.0.5",
"https-proxy-agent": "^5.0.1",
"proxy-from-env": "^2.1.0"
}
},
"node_modules/call-bind-apply-helpers": {
"version": "1.0.2",
"resolved": "https://registry.npmmirror.com/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz",
"integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==",
"dependencies": {
"es-errors": "^1.3.0",
"function-bind": "^1.1.2"
},
"engines": {
"node": ">= 0.4"
}
},
"node_modules/combined-stream": {
"version": "1.0.8",
"resolved": "https://registry.npmmirror.com/combined-stream/-/combined-stream-1.0.8.tgz",
"integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==",
"dependencies": {
"delayed-stream": "~1.0.0"
},
"engines": {
"node": ">= 0.8"
}
},
"node_modules/debug": {
"version": "4.4.3",
"resolved": "https://registry.npmmirror.com/debug/-/debug-4.4.3.tgz",
"integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==",
"dependencies": {
"ms": "^2.1.3"
},
"engines": {
"node": ">=6.0"
},
"peerDependenciesMeta": {
"supports-color": {
"optional": true
}
}
},
"node_modules/delayed-stream": {
"version": "1.0.0",
"resolved": "https://registry.npmmirror.com/delayed-stream/-/delayed-stream-1.0.0.tgz",
"integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==",
"engines": {
"node": ">=0.4.0"
}
},
"node_modules/dunder-proto": {
"version": "1.0.1",
"resolved": "https://registry.npmmirror.com/dunder-proto/-/dunder-proto-1.0.1.tgz",
"integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==",
"dependencies": {
"call-bind-apply-helpers": "^1.0.1",
"es-errors": "^1.3.0",
"gopd": "^1.2.0"
},
"engines": {
"node": ">= 0.4"
}
},
"node_modules/es-define-property": {
"version": "1.0.1",
"resolved": "https://registry.npmmirror.com/es-define-property/-/es-define-property-1.0.1.tgz",
"integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==",
"engines": {
"node": ">= 0.4"
}
},
"node_modules/es-errors": {
"version": "1.3.0",
"resolved": "https://registry.npmmirror.com/es-errors/-/es-errors-1.3.0.tgz",
"integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==",
"engines": {
"node": ">= 0.4"
}
},
"node_modules/es-object-atoms": {
"version": "1.1.2",
"resolved": "https://registry.npmmirror.com/es-object-atoms/-/es-object-atoms-1.1.2.tgz",
"integrity": "sha512-HWcBoN6NileqtSydK2FqHbS/LoDd2pqrnQHLyJzBj4kOp/ky2MWMN694xOfkK8/SnUsW2DH7EfyVlydKCsm1Zw==",
"dependencies": {
"es-errors": "^1.3.0"
},
"engines": {
"node": ">= 0.4"
}
},
"node_modules/es-set-tostringtag": {
"version": "2.1.0",
"resolved": "https://registry.npmmirror.com/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz",
"integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==",
"dependencies": {
"es-errors": "^1.3.0",
"get-intrinsic": "^1.2.6",
"has-tostringtag": "^1.0.2",
"hasown": "^2.0.2"
},
"engines": {
"node": ">= 0.4"
}
},
"node_modules/follow-redirects": {
"version": "1.16.0",
"resolved": "https://registry.npmmirror.com/follow-redirects/-/follow-redirects-1.16.0.tgz",
"integrity": "sha512-y5rN/uOsadFT/JfYwhxRS5R7Qce+g3zG97+JrtFZlC9klX/W5hD7iiLzScI4nZqUS7DNUdhPgw4xI8W2LuXlUw==",
"funding": [
{
"type": "individual",
"url": "https://github.com/sponsors/RubenVerborgh"
}
],
"engines": {
"node": ">=4.0"
},
"peerDependenciesMeta": {
"debug": {
"optional": true
}
}
},
"node_modules/form-data": {
"version": "4.0.5",
"resolved": "https://registry.npmmirror.com/form-data/-/form-data-4.0.5.tgz",
"integrity": "sha512-8RipRLol37bNs2bhoV67fiTEvdTrbMUYcFTiy3+wuuOnUog2QBHCZWXDRijWQfAkhBj2Uf5UnVaiWwA5vdd82w==",
"dependencies": {
"asynckit": "^0.4.0",
"combined-stream": "^1.0.8",
"es-set-tostringtag": "^2.1.0",
"hasown": "^2.0.2",
"mime-types": "^2.1.12"
},
"engines": {
"node": ">= 6"
}
},
"node_modules/function-bind": {
"version": "1.1.2",
"resolved": "https://registry.npmmirror.com/function-bind/-/function-bind-1.1.2.tgz",
"integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==",
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/get-intrinsic": {
"version": "1.3.0",
"resolved": "https://registry.npmmirror.com/get-intrinsic/-/get-intrinsic-1.3.0.tgz",
"integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==",
"dependencies": {
"call-bind-apply-helpers": "^1.0.2",
"es-define-property": "^1.0.1",
"es-errors": "^1.3.0",
"es-object-atoms": "^1.1.1",
"function-bind": "^1.1.2",
"get-proto": "^1.0.1",
"gopd": "^1.2.0",
"has-symbols": "^1.1.0",
"hasown": "^2.0.2",
"math-intrinsics": "^1.1.0"
},
"engines": {
"node": ">= 0.4"
},
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/get-proto": {
"version": "1.0.1",
"resolved": "https://registry.npmmirror.com/get-proto/-/get-proto-1.0.1.tgz",
"integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==",
"dependencies": {
"dunder-proto": "^1.0.1",
"es-object-atoms": "^1.0.0"
},
"engines": {
"node": ">= 0.4"
}
},
"node_modules/gopd": {
"version": "1.2.0",
"resolved": "https://registry.npmmirror.com/gopd/-/gopd-1.2.0.tgz",
"integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==",
"engines": {
"node": ">= 0.4"
},
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/has-symbols": {
"version": "1.1.0",
"resolved": "https://registry.npmmirror.com/has-symbols/-/has-symbols-1.1.0.tgz",
"integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==",
"engines": {
"node": ">= 0.4"
},
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/has-tostringtag": {
"version": "1.0.2",
"resolved": "https://registry.npmmirror.com/has-tostringtag/-/has-tostringtag-1.0.2.tgz",
"integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==",
"dependencies": {
"has-symbols": "^1.0.3"
},
"engines": {
"node": ">= 0.4"
},
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/hasown": {
"version": "2.0.3",
"resolved": "https://registry.npmmirror.com/hasown/-/hasown-2.0.3.tgz",
"integrity": "sha512-ej4AhfhfL2Q2zpMmLo7U1Uv9+PyhIZpgQLGT1F9miIGmiCJIoCgSmczFdrc97mWT4kVY72KA+WnnhJ5pghSvSg==",
"dependencies": {
"function-bind": "^1.1.2"
},
"engines": {
"node": ">= 0.4"
}
},
"node_modules/https-proxy-agent": {
"version": "5.0.1",
"resolved": "https://registry.npmmirror.com/https-proxy-agent/-/https-proxy-agent-5.0.1.tgz",
"integrity": "sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA==",
"dependencies": {
"agent-base": "6",
"debug": "4"
},
"engines": {
"node": ">= 6"
}
},
"node_modules/math-intrinsics": {
"version": "1.1.0",
"resolved": "https://registry.npmmirror.com/math-intrinsics/-/math-intrinsics-1.1.0.tgz",
"integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==",
"engines": {
"node": ">= 0.4"
}
},
"node_modules/mime-db": {
"version": "1.52.0",
"resolved": "https://registry.npmmirror.com/mime-db/-/mime-db-1.52.0.tgz",
"integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==",
"engines": {
"node": ">= 0.6"
}
},
"node_modules/mime-types": {
"version": "2.1.35",
"resolved": "https://registry.npmmirror.com/mime-types/-/mime-types-2.1.35.tgz",
"integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==",
"dependencies": {
"mime-db": "1.52.0"
},
"engines": {
"node": ">= 0.6"
}
},
"node_modules/ms": {
"version": "2.1.3",
"resolved": "https://registry.npmmirror.com/ms/-/ms-2.1.3.tgz",
"integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="
},
"node_modules/proxy-from-env": {
"version": "2.1.0",
"resolved": "https://registry.npmmirror.com/proxy-from-env/-/proxy-from-env-2.1.0.tgz",
"integrity": "sha512-cJ+oHTW1VAEa8cJslgmUZrc+sjRKgAKl3Zyse6+PV38hZe/V6Z14TbCuXcan9F9ghlz4QrFr2c92TNF82UkYHA==",
"engines": {
"node": ">=10"
}
}
}
}
-145
View File
@@ -1,145 +0,0 @@
agent-base
==========
### Turn a function into an [`http.Agent`][http.Agent] instance
[![Build Status](https://github.com/TooTallNate/node-agent-base/workflows/Node%20CI/badge.svg)](https://github.com/TooTallNate/node-agent-base/actions?workflow=Node+CI)
This module provides an `http.Agent` generator. That is, you pass it an async
callback function, and it returns a new `http.Agent` instance that will invoke the
given callback function when sending outbound HTTP requests.
#### Some subclasses:
Here's some more interesting uses of `agent-base`.
Send a pull request to list yours!
* [`http-proxy-agent`][http-proxy-agent]: An HTTP(s) proxy `http.Agent` implementation for HTTP endpoints
* [`https-proxy-agent`][https-proxy-agent]: An HTTP(s) proxy `http.Agent` implementation for HTTPS endpoints
* [`pac-proxy-agent`][pac-proxy-agent]: A PAC file proxy `http.Agent` implementation for HTTP and HTTPS
* [`socks-proxy-agent`][socks-proxy-agent]: A SOCKS proxy `http.Agent` implementation for HTTP and HTTPS
Installation
------------
Install with `npm`:
``` bash
$ npm install agent-base
```
Example
-------
Here's a minimal example that creates a new `net.Socket` connection to the server
for every HTTP request (i.e. the equivalent of `agent: false` option):
```js
var net = require('net');
var tls = require('tls');
var url = require('url');
var http = require('http');
var agent = require('agent-base');
var endpoint = 'http://nodejs.org/api/';
var parsed = url.parse(endpoint);
// This is the important part!
parsed.agent = agent(function (req, opts) {
var socket;
// `secureEndpoint` is true when using the https module
if (opts.secureEndpoint) {
socket = tls.connect(opts);
} else {
socket = net.connect(opts);
}
return socket;
});
// Everything else works just like normal...
http.get(parsed, function (res) {
console.log('"response" event!', res.headers);
res.pipe(process.stdout);
});
```
Returning a Promise or using an `async` function is also supported:
```js
agent(async function (req, opts) {
await sleep(1000);
// etc…
});
```
Return another `http.Agent` instance to "pass through" the responsibility
for that HTTP request to that agent:
```js
agent(function (req, opts) {
return opts.secureEndpoint ? https.globalAgent : http.globalAgent;
});
```
API
---
## Agent(Function callback[, Object options]) → [http.Agent][]
Creates a base `http.Agent` that will execute the callback function `callback`
for every HTTP request that it is used as the `agent` for. The callback function
is responsible for creating a `stream.Duplex` instance of some kind that will be
used as the underlying socket in the HTTP request.
The `options` object accepts the following properties:
* `timeout` - Number - Timeout for the `callback()` function in milliseconds. Defaults to Infinity (optional).
The callback function should have the following signature:
### callback(http.ClientRequest req, Object options, Function cb) → undefined
The ClientRequest `req` can be accessed to read request headers and
and the path, etc. The `options` object contains the options passed
to the `http.request()`/`https.request()` function call, and is formatted
to be directly passed to `net.connect()`/`tls.connect()`, or however
else you want a Socket to be created. Pass the created socket to
the callback function `cb` once created, and the HTTP request will
continue to proceed.
If the `https` module is used to invoke the HTTP request, then the
`secureEndpoint` property on `options` _will be set to `true`_.
License
-------
(The MIT License)
Copyright (c) 2013 Nathan Rajlich &lt;nathan@tootallnate.net&gt;
Permission is hereby granted, free of charge, to any person obtaining
a copy of this software and associated documentation files (the
'Software'), to deal in the Software without restriction, including
without limitation the rights to use, copy, modify, merge, publish,
distribute, sublicense, and/or sell copies of the Software, and to
permit persons to whom the Software is furnished to do so, subject to
the following conditions:
The above copyright notice and this permission notice shall be
included in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND,
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
[http-proxy-agent]: https://github.com/TooTallNate/node-http-proxy-agent
[https-proxy-agent]: https://github.com/TooTallNate/node-https-proxy-agent
[pac-proxy-agent]: https://github.com/TooTallNate/node-pac-proxy-agent
[socks-proxy-agent]: https://github.com/TooTallNate/node-socks-proxy-agent
[http.Agent]: https://nodejs.org/api/http.html#http_class_http_agent
-78
View File
@@ -1,78 +0,0 @@
/// <reference types="node" />
import net from 'net';
import http from 'http';
import https from 'https';
import { Duplex } from 'stream';
import { EventEmitter } from 'events';
declare function createAgent(opts?: createAgent.AgentOptions): createAgent.Agent;
declare function createAgent(callback: createAgent.AgentCallback, opts?: createAgent.AgentOptions): createAgent.Agent;
declare namespace createAgent {
interface ClientRequest extends http.ClientRequest {
_last?: boolean;
_hadError?: boolean;
method: string;
}
interface AgentRequestOptions {
host?: string;
path?: string;
port: number;
}
interface HttpRequestOptions extends AgentRequestOptions, Omit<http.RequestOptions, keyof AgentRequestOptions> {
secureEndpoint: false;
}
interface HttpsRequestOptions extends AgentRequestOptions, Omit<https.RequestOptions, keyof AgentRequestOptions> {
secureEndpoint: true;
}
type RequestOptions = HttpRequestOptions | HttpsRequestOptions;
type AgentLike = Pick<createAgent.Agent, 'addRequest'> | http.Agent;
type AgentCallbackReturn = Duplex | AgentLike;
type AgentCallbackCallback = (err?: Error | null, socket?: createAgent.AgentCallbackReturn) => void;
type AgentCallbackPromise = (req: createAgent.ClientRequest, opts: createAgent.RequestOptions) => createAgent.AgentCallbackReturn | Promise<createAgent.AgentCallbackReturn>;
type AgentCallback = typeof Agent.prototype.callback;
type AgentOptions = {
timeout?: number;
};
/**
* Base `http.Agent` implementation.
* No pooling/keep-alive is implemented by default.
*
* @param {Function} callback
* @api public
*/
class Agent extends EventEmitter {
timeout: number | null;
maxFreeSockets: number;
maxTotalSockets: number;
maxSockets: number;
sockets: {
[key: string]: net.Socket[];
};
freeSockets: {
[key: string]: net.Socket[];
};
requests: {
[key: string]: http.IncomingMessage[];
};
options: https.AgentOptions;
private promisifiedCallback?;
private explicitDefaultPort?;
private explicitProtocol?;
constructor(callback?: createAgent.AgentCallback | createAgent.AgentOptions, _opts?: createAgent.AgentOptions);
get defaultPort(): number;
set defaultPort(v: number);
get protocol(): string;
set protocol(v: string);
callback(req: createAgent.ClientRequest, opts: createAgent.RequestOptions, fn: createAgent.AgentCallbackCallback): void;
callback(req: createAgent.ClientRequest, opts: createAgent.RequestOptions): createAgent.AgentCallbackReturn | Promise<createAgent.AgentCallbackReturn>;
/**
* Called by node-core's "_http_client.js" module when creating
* a new HTTP request with this Agent instance.
*
* @api public
*/
addRequest(req: ClientRequest, _opts: RequestOptions): void;
freeSocket(socket: net.Socket, opts: AgentOptions): void;
destroy(): void;
}
}
export = createAgent;
-203
View File
@@ -1,203 +0,0 @@
"use strict";
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
const events_1 = require("events");
const debug_1 = __importDefault(require("debug"));
const promisify_1 = __importDefault(require("./promisify"));
const debug = debug_1.default('agent-base');
function isAgent(v) {
return Boolean(v) && typeof v.addRequest === 'function';
}
function isSecureEndpoint() {
const { stack } = new Error();
if (typeof stack !== 'string')
return false;
return stack.split('\n').some(l => l.indexOf('(https.js:') !== -1 || l.indexOf('node:https:') !== -1);
}
function createAgent(callback, opts) {
return new createAgent.Agent(callback, opts);
}
(function (createAgent) {
/**
* Base `http.Agent` implementation.
* No pooling/keep-alive is implemented by default.
*
* @param {Function} callback
* @api public
*/
class Agent extends events_1.EventEmitter {
constructor(callback, _opts) {
super();
let opts = _opts;
if (typeof callback === 'function') {
this.callback = callback;
}
else if (callback) {
opts = callback;
}
// Timeout for the socket to be returned from the callback
this.timeout = null;
if (opts && typeof opts.timeout === 'number') {
this.timeout = opts.timeout;
}
// These aren't actually used by `agent-base`, but are required
// for the TypeScript definition files in `@types/node` :/
this.maxFreeSockets = 1;
this.maxSockets = 1;
this.maxTotalSockets = Infinity;
this.sockets = {};
this.freeSockets = {};
this.requests = {};
this.options = {};
}
get defaultPort() {
if (typeof this.explicitDefaultPort === 'number') {
return this.explicitDefaultPort;
}
return isSecureEndpoint() ? 443 : 80;
}
set defaultPort(v) {
this.explicitDefaultPort = v;
}
get protocol() {
if (typeof this.explicitProtocol === 'string') {
return this.explicitProtocol;
}
return isSecureEndpoint() ? 'https:' : 'http:';
}
set protocol(v) {
this.explicitProtocol = v;
}
callback(req, opts, fn) {
throw new Error('"agent-base" has no default implementation, you must subclass and override `callback()`');
}
/**
* Called by node-core's "_http_client.js" module when creating
* a new HTTP request with this Agent instance.
*
* @api public
*/
addRequest(req, _opts) {
const opts = Object.assign({}, _opts);
if (typeof opts.secureEndpoint !== 'boolean') {
opts.secureEndpoint = isSecureEndpoint();
}
if (opts.host == null) {
opts.host = 'localhost';
}
if (opts.port == null) {
opts.port = opts.secureEndpoint ? 443 : 80;
}
if (opts.protocol == null) {
opts.protocol = opts.secureEndpoint ? 'https:' : 'http:';
}
if (opts.host && opts.path) {
// If both a `host` and `path` are specified then it's most
// likely the result of a `url.parse()` call... we need to
// remove the `path` portion so that `net.connect()` doesn't
// attempt to open that as a unix socket file.
delete opts.path;
}
delete opts.agent;
delete opts.hostname;
delete opts._defaultAgent;
delete opts.defaultPort;
delete opts.createConnection;
// Hint to use "Connection: close"
// XXX: non-documented `http` module API :(
req._last = true;
req.shouldKeepAlive = false;
let timedOut = false;
let timeoutId = null;
const timeoutMs = opts.timeout || this.timeout;
const onerror = (err) => {
if (req._hadError)
return;
req.emit('error', err);
// For Safety. Some additional errors might fire later on
// and we need to make sure we don't double-fire the error event.
req._hadError = true;
};
const ontimeout = () => {
timeoutId = null;
timedOut = true;
const err = new Error(`A "socket" was not created for HTTP request before ${timeoutMs}ms`);
err.code = 'ETIMEOUT';
onerror(err);
};
const callbackError = (err) => {
if (timedOut)
return;
if (timeoutId !== null) {
clearTimeout(timeoutId);
timeoutId = null;
}
onerror(err);
};
const onsocket = (socket) => {
if (timedOut)
return;
if (timeoutId != null) {
clearTimeout(timeoutId);
timeoutId = null;
}
if (isAgent(socket)) {
// `socket` is actually an `http.Agent` instance, so
// relinquish responsibility for this `req` to the Agent
// from here on
debug('Callback returned another Agent instance %o', socket.constructor.name);
socket.addRequest(req, opts);
return;
}
if (socket) {
socket.once('free', () => {
this.freeSocket(socket, opts);
});
req.onSocket(socket);
return;
}
const err = new Error(`no Duplex stream was returned to agent-base for \`${req.method} ${req.path}\``);
onerror(err);
};
if (typeof this.callback !== 'function') {
onerror(new Error('`callback` is not defined'));
return;
}
if (!this.promisifiedCallback) {
if (this.callback.length >= 3) {
debug('Converting legacy callback function to promise');
this.promisifiedCallback = promisify_1.default(this.callback);
}
else {
this.promisifiedCallback = this.callback;
}
}
if (typeof timeoutMs === 'number' && timeoutMs > 0) {
timeoutId = setTimeout(ontimeout, timeoutMs);
}
if ('port' in opts && typeof opts.port !== 'number') {
opts.port = Number(opts.port);
}
try {
debug('Resolving socket for %o request: %o', opts.protocol, `${req.method} ${req.path}`);
Promise.resolve(this.promisifiedCallback(req, opts)).then(onsocket, callbackError);
}
catch (err) {
Promise.reject(err).catch(callbackError);
}
}
freeSocket(socket, opts) {
debug('Freeing socket %o %o', socket.constructor.name, opts);
socket.destroy();
}
destroy() {
debug('Destroying agent %o', this.constructor.name);
}
}
createAgent.Agent = Agent;
// So that `instanceof` works correctly
createAgent.prototype = createAgent.Agent.prototype;
})(createAgent || (createAgent = {}));
module.exports = createAgent;
//# sourceMappingURL=index.js.map
-1
View File
@@ -1 +0,0 @@
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/index.ts"],"names":[],"mappings":";;;;AAIA,mCAAsC;AACtC,kDAAgC;AAChC,4DAAoC;AAEpC,MAAM,KAAK,GAAG,eAAW,CAAC,YAAY,CAAC,CAAC;AAExC,SAAS,OAAO,CAAC,CAAM;IACtB,OAAO,OAAO,CAAC,CAAC,CAAC,IAAI,OAAO,CAAC,CAAC,UAAU,KAAK,UAAU,CAAC;AACzD,CAAC;AAED,SAAS,gBAAgB;IACxB,MAAM,EAAE,KAAK,EAAE,GAAG,IAAI,KAAK,EAAE,CAAC;IAC9B,IAAI,OAAO,KAAK,KAAK,QAAQ;QAAE,OAAO,KAAK,CAAC;IAC5C,OAAO,KAAK,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,OAAO,CAAC,YAAY,CAAC,KAAK,CAAC,CAAC,IAAK,CAAC,CAAC,OAAO,CAAC,aAAa,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC;AACxG,CAAC;AAOD,SAAS,WAAW,CACnB,QAA+D,EAC/D,IAA+B;IAE/B,OAAO,IAAI,WAAW,CAAC,KAAK,CAAC,QAAQ,EAAE,IAAI,CAAC,CAAC;AAC9C,CAAC;AAED,WAAU,WAAW;IAmDpB;;;;;;OAMG;IACH,MAAa,KAAM,SAAQ,qBAAY;QAmBtC,YACC,QAA+D,EAC/D,KAAgC;YAEhC,KAAK,EAAE,CAAC;YAER,IAAI,IAAI,GAAG,KAAK,CAAC;YACjB,IAAI,OAAO,QAAQ,KAAK,UAAU,EAAE;gBACnC,IAAI,CAAC,QAAQ,GAAG,QAAQ,CAAC;aACzB;iBAAM,IAAI,QAAQ,EAAE;gBACpB,IAAI,GAAG,QAAQ,CAAC;aAChB;YAED,0DAA0D;YAC1D,IAAI,CAAC,OAAO,GAAG,IAAI,CAAC;YACpB,IAAI,IAAI,IAAI,OAAO,IAAI,CAAC,OAAO,KAAK,QAAQ,EAAE;gBAC7C,IAAI,CAAC,OAAO,GAAG,IAAI,CAAC,OAAO,CAAC;aAC5B;YAED,+DAA+D;YAC/D,0DAA0D;YAC1D,IAAI,CAAC,cAAc,GAAG,CAAC,CAAC;YACxB,IAAI,CAAC,UAAU,GAAG,CAAC,CAAC;YACpB,IAAI,CAAC,eAAe,GAAG,QAAQ,CAAC;YAChC,IAAI,CAAC,OAAO,GAAG,EAAE,CAAC;YAClB,IAAI,CAAC,WAAW,GAAG,EAAE,CAAC;YACtB,IAAI,CAAC,QAAQ,GAAG,EAAE,CAAC;YACnB,IAAI,CAAC,OAAO,GAAG,EAAE,CAAC;QACnB,CAAC;QAED,IAAI,WAAW;YACd,IAAI,OAAO,IAAI,CAAC,mBAAmB,KAAK,QAAQ,EAAE;gBACjD,OAAO,IAAI,CAAC,mBAAmB,CAAC;aAChC;YACD,OAAO,gBAAgB,EAAE,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,CAAC;QACtC,CAAC;QAED,IAAI,WAAW,CAAC,CAAS;YACxB,IAAI,CAAC,mBAAmB,GAAG,CAAC,CAAC;QAC9B,CAAC;QAED,IAAI,QAAQ;YACX,IAAI,OAAO,IAAI,CAAC,gBAAgB,KAAK,QAAQ,EAAE;gBAC9C,OAAO,IAAI,CAAC,gBAAgB,CAAC;aAC7B;YACD,OAAO,gBAAgB,EAAE,CAAC,CAAC,CAAC,QAAQ,CAAC,CAAC,CAAC,OAAO,CAAC;QAChD,CAAC;QAED,IAAI,QAAQ,CAAC,CAAS;YACrB,IAAI,CAAC,gBAAgB,GAAG,CAAC,CAAC;QAC3B,CAAC;QAaD,QAAQ,CACP,GAA8B,EAC9B,IAA8B,EAC9B,EAAsC;YAKtC,MAAM,IAAI,KAAK,CACd,yFAAyF,CACzF,CAAC;QACH,CAAC;QAED;;;;;WAKG;QACH,UAAU,CAAC,GAAkB,EAAE,KAAqB;YACnD,MAAM,IAAI,qBAAwB,KAAK,CAAE,CAAC;YAE1C,IAAI,OAAO,IAAI,CAAC,cAAc,KAAK,SAAS,EAAE;gBAC7C,IAAI,CAAC,cAAc,GAAG,gBAAgB,EAAE,CAAC;aACzC;YAED,IAAI,IAAI,CAAC,IAAI,IAAI,IAAI,EAAE;gBACtB,IAAI,CAAC,IAAI,GAAG,WAAW,CAAC;aACxB;YAED,IAAI,IAAI,CAAC,IAAI,IAAI,IAAI,EAAE;gBACtB,IAAI,CAAC,IAAI,GAAG,IAAI,CAAC,cAAc,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,CAAC;aAC3C;YAED,IAAI,IAAI,CAAC,QAAQ,IAAI,IAAI,EAAE;gBAC1B,IAAI,CAAC,QAAQ,GAAG,IAAI,CAAC,cAAc,CAAC,CAAC,CAAC,QAAQ,CAAC,CAAC,CAAC,OAAO,CAAC;aACzD;YAED,IAAI,IAAI,CAAC,IAAI,IAAI,IAAI,CAAC,IAAI,EAAE;gBAC3B,2DAA2D;gBAC3D,0DAA0D;gBAC1D,4DAA4D;gBAC5D,8CAA8C;gBAC9C,OAAO,IAAI,CAAC,IAAI,CAAC;aACjB;YAED,OAAO,IAAI,CAAC,KAAK,CAAC;YAClB,OAAO,IAAI,CAAC,QAAQ,CAAC;YACrB,OAAO,IAAI,CAAC,aAAa,CAAC;YAC1B,OAAO,IAAI,CAAC,WAAW,CAAC;YACxB,OAAO,IAAI,CAAC,gBAAgB,CAAC;YAE7B,kCAAkC;YAClC,2CAA2C;YAC3C,GAAG,CAAC,KAAK,GAAG,IAAI,CAAC;YACjB,GAAG,CAAC,eAAe,GAAG,KAAK,CAAC;YAE5B,IAAI,QAAQ,GAAG,KAAK,CAAC;YACrB,IAAI,SAAS,GAAyC,IAAI,CAAC;YAC3D,MAAM,SAAS,GAAG,IAAI,CAAC,OAAO,IAAI,IAAI,CAAC,OAAO,CAAC;YAE/C,MAAM,OAAO,GAAG,CAAC,GAA0B,EAAE,EAAE;gBAC9C,IAAI,GAAG,CAAC,SAAS;oBAAE,OAAO;gBAC1B,GAAG,CAAC,IAAI,CAAC,OAAO,EAAE,GAAG,CAAC,CAAC;gBACvB,yDAAyD;gBACzD,iEAAiE;gBACjE,GAAG,CAAC,SAAS,GAAG,IAAI,CAAC;YACtB,CAAC,CAAC;YAEF,MAAM,SAAS,GAAG,GAAG,EAAE;gBACtB,SAAS,GAAG,IAAI,CAAC;gBACjB,QAAQ,GAAG,IAAI,CAAC;gBAChB,MAAM,GAAG,GAA0B,IAAI,KAAK,CAC3C,sDAAsD,SAAS,IAAI,CACnE,CAAC;gBACF,GAAG,CAAC,IAAI,GAAG,UAAU,CAAC;gBACtB,OAAO,CAAC,GAAG,CAAC,CAAC;YACd,CAAC,CAAC;YAEF,MAAM,aAAa,GAAG,CAAC,GAA0B,EAAE,EAAE;gBACpD,IAAI,QAAQ;oBAAE,OAAO;gBACrB,IAAI,SAAS,KAAK,IAAI,EAAE;oBACvB,YAAY,CAAC,SAAS,CAAC,CAAC;oBACxB,SAAS,GAAG,IAAI,CAAC;iBACjB;gBACD,OAAO,CAAC,GAAG,CAAC,CAAC;YACd,CAAC,CAAC;YAEF,MAAM,QAAQ,GAAG,CAAC,MAA2B,EAAE,EAAE;gBAChD,IAAI,QAAQ;oBAAE,OAAO;gBACrB,IAAI,SAAS,IAAI,IAAI,EAAE;oBACtB,YAAY,CAAC,SAAS,CAAC,CAAC;oBACxB,SAAS,GAAG,IAAI,CAAC;iBACjB;gBAED,IAAI,OAAO,CAAC,MAAM,CAAC,EAAE;oBACpB,oDAAoD;oBACpD,wDAAwD;oBACxD,eAAe;oBACf,KAAK,CACJ,6CAA6C,EAC7C,MAAM,CAAC,WAAW,CAAC,IAAI,CACvB,CAAC;oBACD,MAA4B,CAAC,UAAU,CAAC,GAAG,EAAE,IAAI,CAAC,CAAC;oBACpD,OAAO;iBACP;gBAED,IAAI,MAAM,EAAE;oBACX,MAAM,CAAC,IAAI,CAAC,MAAM,EAAE,GAAG,EAAE;wBACxB,IAAI,CAAC,UAAU,CAAC,MAAoB,EAAE,IAAI,CAAC,CAAC;oBAC7C,CAAC,CAAC,CAAC;oBACH,GAAG,CAAC,QAAQ,CAAC,MAAoB,CAAC,CAAC;oBACnC,OAAO;iBACP;gBAED,MAAM,GAAG,GAAG,IAAI,KAAK,CACpB,qDAAqD,GAAG,CAAC,MAAM,IAAI,GAAG,CAAC,IAAI,IAAI,CAC/E,CAAC;gBACF,OAAO,CAAC,GAAG,CAAC,CAAC;YACd,CAAC,CAAC;YAEF,IAAI,OAAO,IAAI,CAAC,QAAQ,KAAK,UAAU,EAAE;gBACxC,OAAO,CAAC,IAAI,KAAK,CAAC,2BAA2B,CAAC,CAAC,CAAC;gBAChD,OAAO;aACP;YAED,IAAI,CAAC,IAAI,CAAC,mBAAmB,EAAE;gBAC9B,IAAI,IAAI,CAAC,QAAQ,CAAC,MAAM,IAAI,CAAC,EAAE;oBAC9B,KAAK,CAAC,gDAAgD,CAAC,CAAC;oBACxD,IAAI,CAAC,mBAAmB,GAAG,mBAAS,CAAC,IAAI,CAAC,QAAQ,CAAC,CAAC;iBACpD;qBAAM;oBACN,IAAI,CAAC,mBAAmB,GAAG,IAAI,CAAC,QAAQ,CAAC;iBACzC;aACD;YAED,IAAI,OAAO,SAAS,KAAK,QAAQ,IAAI,SAAS,GAAG,CAAC,EAAE;gBACnD,SAAS,GAAG,UAAU,CAAC,SAAS,EAAE,SAAS,CAAC,CAAC;aAC7C;YAED,IAAI,MAAM,IAAI,IAAI,IAAI,OALine truncated
-4
View File
@@ -1,4 +0,0 @@
import { ClientRequest, RequestOptions, AgentCallbackCallback, AgentCallbackPromise } from './index';
declare type LegacyCallback = (req: ClientRequest, opts: RequestOptions, fn: AgentCallbackCallback) => void;
export default function promisify(fn: LegacyCallback): AgentCallbackPromise;
export {};
-18
View File
@@ -1,18 +0,0 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
function promisify(fn) {
return function (req, opts) {
return new Promise((resolve, reject) => {
fn.call(this, req, opts, (err, rtn) => {
if (err) {
reject(err);
}
else {
resolve(rtn);
}
});
});
};
}
exports.default = promisify;
//# sourceMappingURL=promisify.js.map
-1
View File
@@ -1 +0,0 @@
{"version":3,"file":"promisify.js","sourceRoot":"","sources":["../../src/promisify.ts"],"names":[],"mappings":";;AAeA,SAAwB,SAAS,CAAC,EAAkB;IACnD,OAAO,UAAsB,GAAkB,EAAE,IAAoB;QACpE,OAAO,IAAI,OAAO,CAAC,CAAC,OAAO,EAAE,MAAM,EAAE,EAAE;YACtC,EAAE,CAAC,IAAI,CACN,IAAI,EACJ,GAAG,EACH,IAAI,EACJ,CAAC,GAA6B,EAAE,GAAyB,EAAE,EAAE;gBAC5D,IAAI,GAAG,EAAE;oBACR,MAAM,CAAC,GAAG,CAAC,CAAC;iBACZ;qBAAM;oBACN,OAAO,CAAC,GAAG,CAAC,CAAC;iBACb;YACF,CAAC,CACD,CAAC;QACH,CAAC,CAAC,CAAC;IACJ,CAAC,CAAC;AACH,CAAC;AAjBD,4BAiBC"}
-64
View File
@@ -1,64 +0,0 @@
{
"name": "agent-base",
"version": "6.0.2",
"description": "Turn a function into an `http.Agent` instance",
"main": "dist/src/index",
"typings": "dist/src/index",
"files": [
"dist/src",
"src"
],
"scripts": {
"prebuild": "rimraf dist",
"build": "tsc",
"postbuild": "cpy --parents src test '!**/*.ts' dist",
"test": "mocha --reporter spec dist/test/*.js",
"test-lint": "eslint src --ext .js,.ts",
"prepublishOnly": "npm run build"
},
"repository": {
"type": "git",
"url": "git://github.com/TooTallNate/node-agent-base.git"
},
"keywords": [
"http",
"agent",
"base",
"barebones",
"https"
],
"author": "Nathan Rajlich <nathan@tootallnate.net> (http://n8.io/)",
"license": "MIT",
"bugs": {
"url": "https://github.com/TooTallNate/node-agent-base/issues"
},
"dependencies": {
"debug": "4"
},
"devDependencies": {
"@types/debug": "4",
"@types/mocha": "^5.2.7",
"@types/node": "^14.0.20",
"@types/semver": "^7.1.0",
"@types/ws": "^6.0.3",
"@typescript-eslint/eslint-plugin": "1.6.0",
"@typescript-eslint/parser": "1.1.0",
"async-listen": "^1.2.0",
"cpy-cli": "^2.0.0",
"eslint": "5.16.0",
"eslint-config-airbnb": "17.1.0",
"eslint-config-prettier": "4.1.0",
"eslint-import-resolver-typescript": "1.1.1",
"eslint-plugin-import": "2.16.0",
"eslint-plugin-jsx-a11y": "6.2.1",
"eslint-plugin-react": "7.12.4",
"mocha": "^6.2.0",
"rimraf": "^3.0.0",
"semver": "^7.1.2",
"typescript": "^3.5.3",
"ws": "^3.0.0"
},
"engines": {
"node": ">= 6.0.0"
}
}
-345
View File
@@ -1,345 +0,0 @@
import net from 'net';
import http from 'http';
import https from 'https';
import { Duplex } from 'stream';
import { EventEmitter } from 'events';
import createDebug from 'debug';
import promisify from './promisify';
const debug = createDebug('agent-base');
function isAgent(v: any): v is createAgent.AgentLike {
return Boolean(v) && typeof v.addRequest === 'function';
}
function isSecureEndpoint(): boolean {
const { stack } = new Error();
if (typeof stack !== 'string') return false;
return stack.split('\n').some(l => l.indexOf('(https.js:') !== -1 || l.indexOf('node:https:') !== -1);
}
function createAgent(opts?: createAgent.AgentOptions): createAgent.Agent;
function createAgent(
callback: createAgent.AgentCallback,
opts?: createAgent.AgentOptions
): createAgent.Agent;
function createAgent(
callback?: createAgent.AgentCallback | createAgent.AgentOptions,
opts?: createAgent.AgentOptions
) {
return new createAgent.Agent(callback, opts);
}
namespace createAgent {
export interface ClientRequest extends http.ClientRequest {
_last?: boolean;
_hadError?: boolean;
method: string;
}
export interface AgentRequestOptions {
host?: string;
path?: string;
// `port` on `http.RequestOptions` can be a string or undefined,
// but `net.TcpNetConnectOpts` expects only a number
port: number;
}
export interface HttpRequestOptions
extends AgentRequestOptions,
Omit<http.RequestOptions, keyof AgentRequestOptions> {
secureEndpoint: false;
}
export interface HttpsRequestOptions
extends AgentRequestOptions,
Omit<https.RequestOptions, keyof AgentRequestOptions> {
secureEndpoint: true;
}
export type RequestOptions = HttpRequestOptions | HttpsRequestOptions;
export type AgentLike = Pick<createAgent.Agent, 'addRequest'> | http.Agent;
export type AgentCallbackReturn = Duplex | AgentLike;
export type AgentCallbackCallback = (
err?: Error | null,
socket?: createAgent.AgentCallbackReturn
) => void;
export type AgentCallbackPromise = (
req: createAgent.ClientRequest,
opts: createAgent.RequestOptions
) =>
| createAgent.AgentCallbackReturn
| Promise<createAgent.AgentCallbackReturn>;
export type AgentCallback = typeof Agent.prototype.callback;
export type AgentOptions = {
timeout?: number;
};
/**
* Base `http.Agent` implementation.
* No pooling/keep-alive is implemented by default.
*
* @param {Function} callback
* @api public
*/
export class Agent extends EventEmitter {
public timeout: number | null;
public maxFreeSockets: number;
public maxTotalSockets: number;
public maxSockets: number;
public sockets: {
[key: string]: net.Socket[];
};
public freeSockets: {
[key: string]: net.Socket[];
};
public requests: {
[key: string]: http.IncomingMessage[];
};
public options: https.AgentOptions;
private promisifiedCallback?: createAgent.AgentCallbackPromise;
private explicitDefaultPort?: number;
private explicitProtocol?: string;
constructor(
callback?: createAgent.AgentCallback | createAgent.AgentOptions,
_opts?: createAgent.AgentOptions
) {
super();
let opts = _opts;
if (typeof callback === 'function') {
this.callback = callback;
} else if (callback) {
opts = callback;
}
// Timeout for the socket to be returned from the callback
this.timeout = null;
if (opts && typeof opts.timeout === 'number') {
this.timeout = opts.timeout;
}
// These aren't actually used by `agent-base`, but are required
// for the TypeScript definition files in `@types/node` :/
this.maxFreeSockets = 1;
this.maxSockets = 1;
this.maxTotalSockets = Infinity;
this.sockets = {};
this.freeSockets = {};
this.requests = {};
this.options = {};
}
get defaultPort(): number {
if (typeof this.explicitDefaultPort === 'number') {
return this.explicitDefaultPort;
}
return isSecureEndpoint() ? 443 : 80;
}
set defaultPort(v: number) {
this.explicitDefaultPort = v;
}
get protocol(): string {
if (typeof this.explicitProtocol === 'string') {
return this.explicitProtocol;
}
return isSecureEndpoint() ? 'https:' : 'http:';
}
set protocol(v: string) {
this.explicitProtocol = v;
}
callback(
req: createAgent.ClientRequest,
opts: createAgent.RequestOptions,
fn: createAgent.AgentCallbackCallback
): void;
callback(
req: createAgent.ClientRequest,
opts: createAgent.RequestOptions
):
| createAgent.AgentCallbackReturn
| Promise<createAgent.AgentCallbackReturn>;
callback(
req: createAgent.ClientRequest,
opts: createAgent.AgentOptions,
fn?: createAgent.AgentCallbackCallback
):
| createAgent.AgentCallbackReturn
| Promise<createAgent.AgentCallbackReturn>
| void {
throw new Error(
'"agent-base" has no default implementation, you must subclass and override `callback()`'
);
}
/**
* Called by node-core's "_http_client.js" module when creating
* a new HTTP request with this Agent instance.
*
* @api public
*/
addRequest(req: ClientRequest, _opts: RequestOptions): void {
const opts: RequestOptions = { ..._opts };
if (typeof opts.secureEndpoint !== 'boolean') {
opts.secureEndpoint = isSecureEndpoint();
}
if (opts.host == null) {
opts.host = 'localhost';
}
if (opts.port == null) {
opts.port = opts.secureEndpoint ? 443 : 80;
}
if (opts.protocol == null) {
opts.protocol = opts.secureEndpoint ? 'https:' : 'http:';
}
if (opts.host && opts.path) {
// If both a `host` and `path` are specified then it's most
// likely the result of a `url.parse()` call... we need to
// remove the `path` portion so that `net.connect()` doesn't
// attempt to open that as a unix socket file.
delete opts.path;
}
delete opts.agent;
delete opts.hostname;
delete opts._defaultAgent;
delete opts.defaultPort;
delete opts.createConnection;
// Hint to use "Connection: close"
// XXX: non-documented `http` module API :(
req._last = true;
req.shouldKeepAlive = false;
let timedOut = false;
let timeoutId: ReturnType<typeof setTimeout> | null = null;
const timeoutMs = opts.timeout || this.timeout;
const onerror = (err: NodeJS.ErrnoException) => {
if (req._hadError) return;
req.emit('error', err);
// For Safety. Some additional errors might fire later on
// and we need to make sure we don't double-fire the error event.
req._hadError = true;
};
const ontimeout = () => {
timeoutId = null;
timedOut = true;
const err: NodeJS.ErrnoException = new Error(
`A "socket" was not created for HTTP request before ${timeoutMs}ms`
);
err.code = 'ETIMEOUT';
onerror(err);
};
const callbackError = (err: NodeJS.ErrnoException) => {
if (timedOut) return;
if (timeoutId !== null) {
clearTimeout(timeoutId);
timeoutId = null;
}
onerror(err);
};
const onsocket = (socket: AgentCallbackReturn) => {
if (timedOut) return;
if (timeoutId != null) {
clearTimeout(timeoutId);
timeoutId = null;
}
if (isAgent(socket)) {
// `socket` is actually an `http.Agent` instance, so
// relinquish responsibility for this `req` to the Agent
// from here on
debug(
'Callback returned another Agent instance %o',
socket.constructor.name
);
(socket as createAgent.Agent).addRequest(req, opts);
return;
}
if (socket) {
socket.once('free', () => {
this.freeSocket(socket as net.Socket, opts);
});
req.onSocket(socket as net.Socket);
return;
}
const err = new Error(
`no Duplex stream was returned to agent-base for \`${req.method} ${req.path}\``
);
onerror(err);
};
if (typeof this.callback !== 'function') {
onerror(new Error('`callback` is not defined'));
return;
}
if (!this.promisifiedCallback) {
if (this.callback.length >= 3) {
debug('Converting legacy callback function to promise');
this.promisifiedCallback = promisify(this.callback);
} else {
this.promisifiedCallback = this.callback;
}
}
if (typeof timeoutMs === 'number' && timeoutMs > 0) {
timeoutId = setTimeout(ontimeout, timeoutMs);
}
if ('port' in opts && typeof opts.port !== 'number') {
opts.port = Number(opts.port);
}
try {
debug(
'Resolving socket for %o request: %o',
opts.protocol,
`${req.method} ${req.path}`
);
Promise.resolve(this.promisifiedCallback(req, opts)).then(
onsocket,
callbackError
);
} catch (err) {
Promise.reject(err).catch(callbackError);
}
}
freeSocket(socket: net.Socket, opts: AgentOptions) {
debug('Freeing socket %o %o', socket.constructor.name, opts);
socket.destroy();
}
destroy() {
debug('Destroying agent %o', this.constructor.name);
}
}
// So that `instanceof` works correctly
createAgent.prototype = createAgent.Agent.prototype;
}
export = createAgent;
-33
View File
@@ -1,33 +0,0 @@
import {
Agent,
ClientRequest,
RequestOptions,
AgentCallbackCallback,
AgentCallbackPromise,
AgentCallbackReturn
} from './index';
type LegacyCallback = (
req: ClientRequest,
opts: RequestOptions,
fn: AgentCallbackCallback
) => void;
export default function promisify(fn: LegacyCallback): AgentCallbackPromise {
return function(this: Agent, req: ClientRequest, opts: RequestOptions) {
return new Promise((resolve, reject) => {
fn.call(
this,
req,
opts,
(err: Error | null | undefined, rtn?: AgentCallbackReturn) => {
if (err) {
reject(err);
} else {
resolve(rtn);
}
}
);
});
};
}
-21
View File
@@ -1,21 +0,0 @@
The MIT License (MIT)
Copyright (c) 2016 Alex Indigo
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
-233
View File
@@ -1,233 +0,0 @@
# asynckit [![NPM Module](https://img.shields.io/npm/v/asynckit.svg?style=flat)](https://www.npmjs.com/package/asynckit)
Minimal async jobs utility library, with streams support.
[![PhantomJS Build](https://img.shields.io/travis/alexindigo/asynckit/v0.4.0.svg?label=browser&style=flat)](https://travis-ci.org/alexindigo/asynckit)
[![Linux Build](https://img.shields.io/travis/alexindigo/asynckit/v0.4.0.svg?label=linux:0.12-6.x&style=flat)](https://travis-ci.org/alexindigo/asynckit)
[![Windows Build](https://img.shields.io/appveyor/ci/alexindigo/asynckit/v0.4.0.svg?label=windows:0.12-6.x&style=flat)](https://ci.appveyor.com/project/alexindigo/asynckit)
[![Coverage Status](https://img.shields.io/coveralls/alexindigo/asynckit/v0.4.0.svg?label=code+coverage&style=flat)](https://coveralls.io/github/alexindigo/asynckit?branch=master)
[![Dependency Status](https://img.shields.io/david/alexindigo/asynckit/v0.4.0.svg?style=flat)](https://david-dm.org/alexindigo/asynckit)
[![bitHound Overall Score](https://www.bithound.io/github/alexindigo/asynckit/badges/score.svg)](https://www.bithound.io/github/alexindigo/asynckit)
<!-- [![Readme](https://img.shields.io/badge/readme-tested-brightgreen.svg?style=flat)](https://www.npmjs.com/package/reamde) -->
AsyncKit provides harness for `parallel` and `serial` iterators over list of items represented by arrays or objects.
Optionally it accepts abort function (should be synchronously return by iterator for each item), and terminates left over jobs upon an error event. For specific iteration order built-in (`ascending` and `descending`) and custom sort helpers also supported, via `asynckit.serialOrdered` method.
It ensures async operations to keep behavior more stable and prevent `Maximum call stack size exceeded` errors, from sync iterators.
| compression | size |
| :----------------- | -------: |
| asynckit.js | 12.34 kB |
| asynckit.min.js | 4.11 kB |
| asynckit.min.js.gz | 1.47 kB |
## Install
```sh
$ npm install --save asynckit
```
## Examples
### Parallel Jobs
Runs iterator over provided array in parallel. Stores output in the `result` array,
on the matching positions. In unlikely event of an error from one of the jobs,
will terminate rest of the active jobs (if abort function is provided)
and return error along with salvaged data to the main callback function.
#### Input Array
```javascript
var parallel = require('asynckit').parallel
, assert = require('assert')
;
var source = [ 1, 1, 4, 16, 64, 32, 8, 2 ]
, expectedResult = [ 2, 2, 8, 32, 128, 64, 16, 4 ]
, expectedTarget = [ 1, 1, 2, 4, 8, 16, 32, 64 ]
, target = []
;
parallel(source, asyncJob, function(err, result)
{
assert.deepEqual(result, expectedResult);
assert.deepEqual(target, expectedTarget);
});
// async job accepts one element from the array
// and a callback function
function asyncJob(item, cb)
{
// different delays (in ms) per item
var delay = item * 25;
// pretend different jobs take different time to finish
// and not in consequential order
var timeoutId = setTimeout(function() {
target.push(item);
cb(null, item * 2);
}, delay);
// allow to cancel "leftover" jobs upon error
// return function, invoking of which will abort this job
return clearTimeout.bind(null, timeoutId);
}
```
More examples could be found in [test/test-parallel-array.js](test/test-parallel-array.js).
#### Input Object
Also it supports named jobs, listed via object.
```javascript
var parallel = require('asynckit/parallel')
, assert = require('assert')
;
var source = { first: 1, one: 1, four: 4, sixteen: 16, sixtyFour: 64, thirtyTwo: 32, eight: 8, two: 2 }
, expectedResult = { first: 2, one: 2, four: 8, sixteen: 32, sixtyFour: 128, thirtyTwo: 64, eight: 16, two: 4 }
, expectedTarget = [ 1, 1, 2, 4, 8, 16, 32, 64 ]
, expectedKeys = [ 'first', 'one', 'two', 'four', 'eight', 'sixteen', 'thirtyTwo', 'sixtyFour' ]
, target = []
, keys = []
;
parallel(source, asyncJob, function(err, result)
{
assert.deepEqual(result, expectedResult);
assert.deepEqual(target, expectedTarget);
assert.deepEqual(keys, expectedKeys);
});
// supports full value, key, callback (shortcut) interface
function asyncJob(item, key, cb)
{
// different delays (in ms) per item
var delay = item * 25;
// pretend different jobs take different time to finish
// and not in consequential order
var timeoutId = setTimeout(function() {
keys.push(key);
target.push(item);
cb(null, item * 2);
}, delay);
// allow to cancel "leftover" jobs upon error
// return function, invoking of which will abort this job
return clearTimeout.bind(null, timeoutId);
}
```
More examples could be found in [test/test-parallel-object.js](test/test-parallel-object.js).
### Serial Jobs
Runs iterator over provided array sequentially. Stores output in the `result` array,
on the matching positions. In unlikely event of an error from one of the jobs,
will not proceed to the rest of the items in the list
and return error along with salvaged data to the main callback function.
#### Input Array
```javascript
var serial = require('asynckit/serial')
, assert = require('assert')
;
var source = [ 1, 1, 4, 16, 64, 32, 8, 2 ]
, expectedResult = [ 2, 2, 8, 32, 128, 64, 16, 4 ]
, expectedTarget = [ 0, 1, 2, 3, 4, 5, 6, 7 ]
, target = []
;
serial(source, asyncJob, function(err, result)
{
assert.deepEqual(result, expectedResult);
assert.deepEqual(target, expectedTarget);
});
// extended interface (item, key, callback)
// also supported for arrays
function asyncJob(item, key, cb)
{
target.push(key);
// it will be automatically made async
// even it iterator "returns" in the same event loop
cb(null, item * 2);
}
```
More examples could be found in [test/test-serial-array.js](test/test-serial-array.js).
#### Input Object
Also it supports named jobs, listed via object.
```javascript
var serial = require('asynckit').serial
, assert = require('assert')
;
var source = [ 1, 1, 4, 16, 64, 32, 8, 2 ]
, expectedResult = [ 2, 2, 8, 32, 128, 64, 16, 4 ]
, expectedTarget = [ 0, 1, 2, 3, 4, 5, 6, 7 ]
, target = []
;
var source = { first: 1, one: 1, four: 4, sixteen: 16, sixtyFour: 64, thirtyTwo: 32, eight: 8, two: 2 }
, expectedResult = { first: 2, one: 2, four: 8, sixteen: 32, sixtyFour: 128, thirtyTwo: 64, eight: 16, two: 4 }
, expectedTarget = [ 1, 1, 4, 16, 64, 32, 8, 2 ]
, target = []
;
serial(source, asyncJob, function(err, result)
{
assert.deepEqual(result, expectedResult);
assert.deepEqual(target, expectedTarget);
});
// shortcut interface (item, callback)
// works for object as well as for the arrays
function asyncJob(item, cb)
{
target.push(item);
// it will be automatically made async
// even it iterator "returns" in the same event loop
cb(null, item * 2);
}
```
More examples could be found in [test/test-serial-object.js](test/test-serial-object.js).
_Note: Since _object_ is an _unordered_ collection of properties,
it may produce unexpected results with sequential iterations.
Whenever order of the jobs' execution is important please use `serialOrdered` method._
### Ordered Serial Iterations
TBD
For example [compare-property](compare-property) package.
### Streaming interface
TBD
## Want to Know More?
More examples can be found in [test folder](test/).
Or open an [issue](https://github.com/alexindigo/asynckit/issues) with questions and/or suggestions.
## License
AsyncKit is licensed under the MIT license.
-76
View File
@@ -1,76 +0,0 @@
/* eslint no-console: "off" */
var asynckit = require('./')
, async = require('async')
, assert = require('assert')
, expected = 0
;
var Benchmark = require('benchmark');
var suite = new Benchmark.Suite;
var source = [];
for (var z = 1; z < 100; z++)
{
source.push(z);
expected += z;
}
suite
// add tests
.add('async.map', function(deferred)
{
var total = 0;
async.map(source,
function(i, cb)
{
setImmediate(function()
{
total += i;
cb(null, total);
});
},
function(err, result)
{
assert.ifError(err);
assert.equal(result[result.length - 1], expected);
deferred.resolve();
});
}, {'defer': true})
.add('asynckit.parallel', function(deferred)
{
var total = 0;
asynckit.parallel(source,
function(i, cb)
{
setImmediate(function()
{
total += i;
cb(null, total);
});
},
function(err, result)
{
assert.ifError(err);
assert.equal(result[result.length - 1], expected);
deferred.resolve();
});
}, {'defer': true})
// add listeners
.on('cycle', function(ev)
{
console.log(String(ev.target));
})
.on('complete', function()
{
console.log('Fastest is ' + this.filter('fastest').map('name'));
})
// run async
.run({ 'async': true });
-6
View File
@@ -1,6 +0,0 @@
module.exports =
{
parallel : require('./parallel.js'),
serial : require('./serial.js'),
serialOrdered : require('./serialOrdered.js')
};
-29
View File
@@ -1,29 +0,0 @@
// API
module.exports = abort;
/**
* Aborts leftover active jobs
*
* @param {object} state - current state object
*/
function abort(state)
{
Object.keys(state.jobs).forEach(clean.bind(state));
// reset leftover jobs
state.jobs = {};
}
/**
* Cleans up leftover job by invoking abort function for the provided job id
*
* @this state
* @param {string|number} key - job id to abort
*/
function clean(key)
{
if (typeof this.jobs[key] == 'function')
{
this.jobs[key]();
}
}
-34
View File
@@ -1,34 +0,0 @@
var defer = require('./defer.js');
// API
module.exports = async;
/**
* Runs provided callback asynchronously
* even if callback itself is not
*
* @param {function} callback - callback to invoke
* @returns {function} - augmented callback
*/
function async(callback)
{
var isAsync = false;
// check if async happened
defer(function() { isAsync = true; });
return function async_callback(err, result)
{
if (isAsync)
{
callback(err, result);
}
else
{
defer(function nextTick_callback()
{
callback(err, result);
});
}
};
}
-26
View File
@@ -1,26 +0,0 @@
module.exports = defer;
/**
* Runs provided function on next iteration of the event loop
*
* @param {function} fn - function to run
*/
function defer(fn)
{
var nextTick = typeof setImmediate == 'function'
? setImmediate
: (
typeof process == 'object' && typeof process.nextTick == 'function'
? process.nextTick
: null
);
if (nextTick)
{
nextTick(fn);
}
else
{
setTimeout(fn, 0);
}
}
-75
View File
@@ -1,75 +0,0 @@
var async = require('./async.js')
, abort = require('./abort.js')
;
// API
module.exports = iterate;
/**
* Iterates over each job object
*
* @param {array|object} list - array or object (named list) to iterate over
* @param {function} iterator - iterator to run
* @param {object} state - current job status
* @param {function} callback - invoked when all elements processed
*/
function iterate(list, iterator, state, callback)
{
// store current index
var key = state['keyedList'] ? state['keyedList'][state.index] : state.index;
state.jobs[key] = runJob(iterator, key, list[key], function(error, output)
{
// don't repeat yourself
// skip secondary callbacks
if (!(key in state.jobs))
{
return;
}
// clean up jobs
delete state.jobs[key];
if (error)
{
// don't process rest of the results
// stop still active jobs
// and reset the list
abort(state);
}
else
{
state.results[key] = output;
}
// return salvaged results
callback(error, state.results);
});
}
/**
* Runs iterator over provided job element
*
* @param {function} iterator - iterator to invoke
* @param {string|number} key - key/index of the element in the list of jobs
* @param {mixed} item - job description
* @param {function} callback - invoked after iterator is done with the job
* @returns {function|mixed} - job abort function or something else
*/
function runJob(iterator, key, item, callback)
{
var aborter;
// allow shortcut if iterator expects only two arguments
if (iterator.length == 2)
{
aborter = iterator(item, async(callback));
}
// otherwise go with full three arguments
else
{
aborter = iterator(item, key, async(callback));
}
return aborter;
}
-91
View File
@@ -1,91 +0,0 @@
var streamify = require('./streamify.js')
, defer = require('./defer.js')
;
// API
module.exports = ReadableAsyncKit;
/**
* Base constructor for all streams
* used to hold properties/methods
*/
function ReadableAsyncKit()
{
ReadableAsyncKit.super_.apply(this, arguments);
// list of active jobs
this.jobs = {};
// add stream methods
this.destroy = destroy;
this._start = _start;
this._read = _read;
}
/**
* Destroys readable stream,
* by aborting outstanding jobs
*
* @returns {void}
*/
function destroy()
{
if (this.destroyed)
{
return;
}
this.destroyed = true;
if (typeof this.terminator == 'function')
{
this.terminator();
}
}
/**
* Starts provided jobs in async manner
*
* @private
*/
function _start()
{
// first argument – runner function
var runner = arguments[0]
// take away first argument
, args = Array.prototype.slice.call(arguments, 1)
// second argument - input data
, input = args[0]
// last argument - result callback
, endCb = streamify.callback.call(this, args[args.length - 1])
;
args[args.length - 1] = endCb;
// third argument - iterator
args[1] = streamify.iterator.call(this, args[1]);
// allow time for proper setup
defer(function()
{
if (!this.destroyed)
{
this.terminator = runner.apply(null, args);
}
else
{
endCb(null, Array.isArray(input) ? [] : {});
}
}.bind(this));
}
/**
* Implement _read to comply with Readable streams
* Doesn't really make sense for flowing object mode
*
* @private
*/
function _read()
{
}
Loaded 100 of 718 files, more files were not shown because too many files have changed in this diff. Show more