| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300 |
- import fs from 'node:fs/promises';
- import path from 'node:path';
- import { pathToFileURL } from 'node:url';
- import { Presentation, PresentationFile } from '@oai/artifact-tool';
- import { createCanvas } from '@napi-rs/canvas';
- const workspaceDir = 'C:/Users/30214/Desktop/sub2本地/sub2新站';
- const buildDir = path.join(workspaceDir, '.codex-artifacts/juheai-astra-20260915');
- const assetDir = path.join(buildDir, 'assets');
- const outputDir = path.join(workspaceDir, 'output/presentations');
- const skillDir = 'C:/Users/30214/.codex/plugins/cache/openai-primary-runtime/presentations/26.909.12148/skills/presentations';
- const pythonExecutable = 'C:/Users/30214/.cache/codex-runtimes/codex-primary-runtime/dependencies/python/python.exe';
- const revision = process.argv[2] || 'v1';
- const finalPath = path.join(outputDir, revision === '交付版' ? '聚合AI_Astra_方案一.pptx' : `聚合AI_Astra_方案一_${revision}.pptx`);
- const renderDir = path.join(buildDir, `rendered-${revision}`);
- const { resolvePresentationFont, finalizePresentation } = await import(pathToFileURL(path.join(skillDir, 'container_tools/artifact_tool_utils.mjs')).href);
- const font = resolvePresentationFont({ fontFamily: 'Microsoft YaHei' });
- const fontPolicy = { basis: 'design', families: [font] };
- const C = { black: '#000000', white: '#F0F4FA', silver: '#B6C3D1', muted: '#8C9BAE', blue: '#88D6FF', dim: '#344554' };
- const presentation = Presentation.create({ slideSize: { width: 1280, height: 720 } });
- const measureContext = createCanvas(1280, 720).getContext('2d');
- const textChecks = [];
- const pendingFolios = [];
- const assets = {};
- const imageCredits = {};
- const source = (rel, line) => `${workspaceDir}/${rel}:${line}`;
- const SOURCES = {
- platform: source('frontend/src/views/HomeView.vue', 62),
- protocols: source('backend/internal/server/routes/gateway.go', 196),
- gemini: source('backend/internal/server/routes/gateway.go', 339),
- clients: source('frontend/src/components/keys/UseKeyModal.vue', 941),
- astra: source('frontend/src/components/keys/UseKeyModal.vue', 1328),
- reasoning: source('backend/internal/service/openai_gateway_request_body.go', 1022),
- retryTest: source('backend/internal/service/openai_astra_pro_retry_test.go', 64),
- failover: source('backend/internal/handler/openai_gateway_handler.go', 883),
- failoverTest: source('backend/internal/handler/openai_responses_astra_pro_failover_test.go', 140),
- stream: source('backend/internal/service/openai_gateway_forward.go', 1207),
- usage: source('frontend/src/views/user/UsageView.vue', 650),
- monitor: source('frontend/src/views/user/ChannelStatusV2View.vue', 196),
- key: source('frontend/src/views/user/KeysView.vue', 205),
- };
- async function loadAsset(name, required = false) {
- for (const ext of ['jpg', 'jpeg', 'png', 'webp']) {
- const file = path.join(assetDir, `${name}.${ext}`);
- try {
- const bytes = await fs.readFile(file);
- return { bytes, contentType: ext === 'jpg' || ext === 'jpeg' ? 'image/jpeg' : `image/${ext}`, file };
- } catch (error) {
- if (error.code !== 'ENOENT') throw error;
- }
- }
- if (required) throw new Error(`Missing required asset: ${name}`);
- return null;
- }
- function text(slide, name, value, left, top, width, height, size, opts = {}) {
- measureContext.font = `${opts.bold ? 'bold' : 'normal'} ${size}px "${font}"`;
- const metrics = measureContext.measureText(value);
- const measuredHeight = metrics.actualBoundingBoxAscent + metrics.actualBoundingBoxDescent;
- textChecks.push({ slide: presentation.slides.items.indexOf(slide) + 1, name, value, left, top, width, height, color: opts.color ?? C.white, measuredWidth: metrics.width, measuredHeight });
- if (metrics.width > width - 2 || measuredHeight > height || left < 0 || top < 0 || left + width > 1280 || top + height > 720) {
- throw new Error(`Text does not fit: ${name}: ${metrics.width}x${measuredHeight} in ${width}x${height}`);
- }
- const box = slide.shapes.add({
- name,
- geometry: 'textbox',
- position: { left, top, width, height },
- fill: 'none',
- line: { fill: 'none', width: 0 },
- });
- box.text = value;
- box.text.style = {
- typeface: font,
- fontSize: size,
- bold: opts.bold ?? false,
- color: opts.color ?? C.white,
- alignment: opts.align ?? 'left',
- verticalAlignment: 'middle',
- autoFit: 'none',
- wrap: 'none',
- insets: { left: 0, right: 0, top: 0, bottom: 0 },
- };
- return box;
- }
- function art(slide, name, left, top, width, height, fit = 'contain') {
- const asset = assets[name];
- if (!asset) return;
- slide.images.add({
- blob: asset.bytes,
- contentType: asset.contentType,
- alt: imageCredits[name]?.title || `Space image background: ${name}`,
- fit,
- position: { left, top, width, height },
- });
- }
- function notes(slide, narration, references = [], artNames = []) {
- const credit = artNames.filter(x => imageCredits[x]).map(x => {
- const a = imageCredits[x];
- return `Visual source: ${a.title}. ${a.credit || ''} ${a.page || ''} ${a.url || ''}`.trim();
- });
- slide.speakerNotes.textFrame.setText([
- narration,
- ...references.map(ref => `Source: ${ref}`),
- ...credit,
- ].join('\n\n'));
- }
- function slideBase(page, withFolio = true) {
- const slide = presentation.slides.add();
- slide.background.fill = C.black;
- if (withFolio) pendingFolios.push({ slide, page });
- return slide;
- }
- function title(slide, value) {
- return text(slide, 'slide-title', value, 76, 72, 1128, 84, 48, { bold: true });
- }
- async function build() {
- await fs.mkdir(outputDir, { recursive: true });
- await fs.mkdir(renderDir, { recursive: true });
- for (const name of ['cover-halo', 'astra-core', 'speed-light']) assets[name] = await loadAsset(name, name === 'cover-halo');
- Object.assign(imageCredits, JSON.parse(await fs.readFile(path.join(assetDir, 'credits.json'), 'utf8')));
- // 1. Platform first. Astra is the promoted model within the platform.
- {
- const s = slideBase(1, false);
- art(s, 'cover-halo', 575, 58, 705, 590);
- text(s, 'brand', '聚合AI', 76, 164, 650, 120, 88, { bold: true });
- text(s, 'featured-model', 'Astra 主推模型', 80, 306, 540, 80, 40);
- text(s, 'brand-promise', '不降智,稳定,高速', 80, 440, 550, 55, 30, { color: C.silver });
- text(s, 'platform-value', '多模型选择,统一接入', 80, 596, 540, 44, 24, { color: C.blue });
- notes(s, '聚合AI是平台品牌,Astra是本次重点介绍的模型。平台聚合多种模型接入方式,让用户在熟悉的工具中完成调用。品牌名称和“不降智,稳定,高速”的定位来自用户。', [SOURCES.platform, SOURCES.protocols], ['cover-halo']);
- }
- // 2. Clear aggregation identity and protocol compatibility.
- {
- const s = slideBase(2);
- title(s, '多模型,一个入口');
- text(s, 'platform-explanation', '模型接入、API Key、调用记录与费用管理,集中在聚合AI。', 78, 174, 1120, 56, 26, { color: C.silver });
- text(s, 'featured-model-label', '本站主推', 80, 316, 480, 42, 24, { color: C.blue });
- text(s, 'astra-word', 'Astra', 76, 354, 510, 112, 88, { bold: true });
- text(s, 'protocol-label', '支持多种接入协议', 682, 282, 510, 43, 24, { color: C.muted });
- text(s, 'protocol-openai', 'OpenAI 兼容', 682, 342, 510, 53, 34);
- text(s, 'protocol-anthropic', 'Anthropic Messages', 682, 412, 510, 53, 34);
- text(s, 'protocol-gemini', 'Gemini 原生', 682, 482, 510, 53, 34);
- text(s, 'platform-takeaway', '按任务选择模型,沿用熟悉的工具。', 80, 595, 1070, 46, 26, { color: C.silver });
- notes(s, '聚合AI提供多种协议路由,统一管理密钥和用量。OpenAI兼容包括Responses和Chat Completions,另有Anthropic Messages与Gemini原生路由。可用模型以账号分组、权限和控制台列表为准。', [SOURCES.platform, SOURCES.protocols, SOURCES.gemini]);
- }
- // 3. A quiet model hero, with task-oriented positioning.
- {
- const s = slideBase(3);
- text(s, 'model-section-label', '聚合AI 主推模型', 80, 88, 730, 50, 28, { color: C.blue });
- text(s, 'astra-hero', 'ASTRA', 72, 178, 1128, 181, 136, { bold: true });
- text(s, 'astra-positioning', '把 Astra 接入你的工作流', 80, 367, 740, 66, 36);
- text(s, 'astra-settings', '按所选模型与推理设置发起调用。', 80, 445, 760, 55, 26, { color: C.silver });
- text(s, 'task-coding', '编码协作', 80, 586, 218, 44, 26, { color: C.blue });
- text(s, 'task-analysis', '复杂分析', 303, 586, 218, 44, 26, { color: C.silver });
- text(s, 'task-agent', 'Agent 任务', 526, 586, 300, 44, 26, { color: C.silver });
- notes(s, 'Astra是用户指定的主推模型。项目客户端配置包含Astra模型标识,转发代码保留相应推理参数。编码协作、复杂分析与Agent任务是本页给出的使用方向,不构成与其他模型的能力排名或测试结论。', [SOURCES.astra, SOURCES.reasoning]);
- }
- // 4. Explain “不降智” narrowly and concretely.
- {
- const s = slideBase(4);
- title(s, '不降智,保留 Astra 推理设置');
- text(s, 'preserve-model', 'Astra 按设定调用', 76, 256, 630, 95, 56, { bold: true });
- text(s, 'preserve-reasoning', '保留推理设置', 76, 366, 630, 95, 56, { bold: true, color: C.blue });
- text(s, 'settings-body-1', 'Astra 请求重试或故障切换时,', 739, 263, 469, 44, 27, { color: C.silver });
- text(s, 'settings-body-2', '继续保留指定推理设置。', 739, 308, 469, 44, 27, { color: C.silver });
- text(s, 'mode-rejected-1', '推理设置不受支持时,', 739, 412, 469, 44, 27, { color: C.silver });
- text(s, 'mode-rejected-2', '明确提示原因。', 739, 457, 469, 44, 27, { color: C.silver });
- notes(s, '这里把“不降智”具体解释为Astra指定推理参数的保留。代码及现有测试覆盖pro/max参数在重试与账号切换时保留,并在上游拒绝指定模式时保留错误。测试也覆盖特定重试和故障切换路径中Astra模型标识不变;项目允许显式模型映射,不将这项测试泛化为所有配置下模型标识永不改变。引用测试文件属于实现依据,本次未运行线上质量测试;不把转发设置等同于上游每次回答质量保证。', [SOURCES.reasoning, SOURCES.retryTest, SOURCES.failoverTest]);
- }
- // 5. An honest workflow example, not a fabricated model response.
- {
- const s = slideBase(5);
- title(s, 'Astra 工作流示例');
- text(s, 'sample-task-line-1', '为已有项目增加登录能力', 76, 229, 1128, 77, 48, { bold: true });
- text(s, 'sample-task-line-2', '兼顾权限、异常处理与验证', 76, 313, 1128, 77, 48, { color: C.blue });
- text(s, 'context-label', '给模型的上下文', 78, 466, 550, 45, 28);
- text(s, 'context-body-1', '现有代码、技术栈、约束', 78, 523, 550, 44, 26, { color: C.silver });
- text(s, 'context-body-2', '与验收要求。', 78, 565, 550, 44, 26, { color: C.silver });
- text(s, 'review-label', '你要检查的结果', 683, 466, 520, 45, 28);
- text(s, 'review-body-1', '实现方案、代码修改、验证步骤', 683, 523, 520, 44, 26, { color: C.silver });
- text(s, 'review-body-2', '与边界说明。', 683, 565, 520, 44, 26, { color: C.silver });
- notes(s, '这一页是面向开发者的任务示例,展示如何组织输入与检查输出,不是Astra实际跑出的回答,也不是性能或正确率测评。讲解时可结合真实项目补充上下文,由用户核验模型给出的方案和代码。', [SOURCES.platform]);
- }
- // 6. Conditional recovery rather than an invented SLA.
- {
- const s = slideBase(6);
- title(s, '稳定调用,异常自动处理');
- text(s, 'recovery-condition', '请求条件允许时,平台识别可重试故障,并尝试切换合适账号。', 78, 173, 1128, 57, 26, { color: C.silver });
- const columns = [
- { x: 78, n: '01', label: '识别异常', detail: '判断请求是否可以重试' },
- { x: 471, n: '02', label: '切换账号', detail: '选择当前可用的账号' },
- { x: 864, n: '03', label: '保持设置', detail: '继续保留 Astra 推理参数' },
- ];
- for (const { x, n, label, detail } of columns) {
- text(s, `recovery-number-${n}`, n, x, 299, 320, 99, 76, { color: C.blue });
- text(s, `recovery-label-${n}`, label, x, 420, 330, 57, 36, { bold: true });
- text(s, `recovery-detail-${n}`, detail, x, 488, 340, 49, 24, { color: C.silver });
- }
- text(s, 'recovery-monitor', '渠道状态与成功率可查看。', 78, 602, 1040, 40, 25, { color: C.muted });
- notes(s, '自动重试与切换有明确条件,包括错误允许重试、请求可安全回放、存在合适账号并且未达到上限。页面不承诺零中断或无限重试。渠道监控提供状态及成功率,具体结果受样本、时间窗口和上游状态影响。', [SOURCES.failover, SOURCES.failoverTest, SOURCES.monitor]);
- }
- // 7. Streaming and observable latency, with no invented measurement.
- {
- const s = slideBase(7);
- art(s, 'speed-light', 685, 448, 519, 181, 'cover');
- title(s, '高速响应,进度看得见');
- text(s, 'stream-label', '流式输出', 78, 244, 520, 69, 44, { bold: true, color: C.blue });
- text(s, 'stream-description-1', '开启流式请求,', 78, 331, 555, 49, 27, { color: C.silver });
- text(s, 'stream-description-2', '生成内容逐步返回。', 78, 378, 555, 49, 27, { color: C.silver });
- text(s, 'latency-label', '响应可查', 685, 244, 518, 69, 44, { bold: true });
- text(s, 'latency-description-1', '首 Token 与总耗时,', 685, 331, 518, 49, 27, { color: C.silver });
- text(s, 'latency-description-2', '可在用量记录中核对。', 685, 378, 518, 49, 27, { color: C.silver });
- text(s, 'stream-closing-1', '使用过程更直观,', 78, 526, 550, 44, 26, { color: C.silver });
- text(s, 'stream-closing-2', '响应表现有记录。', 78, 569, 550, 44, 26, { color: C.silver });
- notes(s, '流式输出要求客户端开启stream并且相应上游协议支持。用量记录展示首Token时间和总耗时。本页不引用未经采集的速度数字,不把渠道监控中TPM除以60得到的窗口吞吐量当作单请求生成速度。', [SOURCES.stream, SOURCES.usage, SOURCES.monitor], ['speed-light']);
- }
- // 8. Return to the wider value of an AI aggregation platform.
- {
- const s = slideBase(8);
- title(s, '多模型接入,调用清楚可查');
- text(s, 'models-label', '按任务选模型', 78, 275, 552, 69, 40, { bold: true, color: C.blue });
- text(s, 'models-description-1', '通过控制台查看可用模型,', 78, 365, 552, 49, 27, { color: C.silver });
- text(s, 'models-description-2', '选择适合当前任务的配置。', 78, 412, 552, 49, 27, { color: C.silver });
- text(s, 'usage-label', '按记录看消耗', 684, 275, 520, 69, 40, { bold: true });
- text(s, 'usage-description-1', '模型、Token 和实际扣费,', 684, 365, 520, 49, 27, { color: C.silver });
- text(s, 'usage-description-2', '每笔调用都可核对。', 684, 412, 520, 49, 27, { color: C.silver });
- text(s, 'key-control', 'API Key 的额度、有效期与访问权限可分别设置。', 78, 579, 1128, 54, 26, { color: C.muted });
- notes(s, '聚合平台统一提供模型列表、密钥管理与调用记录。用量记录包括输入输出及缓存Token和实际费用。可用模型按账号分组与权限确定,费用以实际配置和用量记录为准。', [SOURCES.platform, SOURCES.usage, SOURCES.key]);
- }
- // 9. An actionable onboarding sequence without an invented URL.
- {
- const s = slideBase(9);
- title(s, '接入你正在用的工具');
- text(s, 'compatibility', '支持自定义服务地址的客户端或 SDK,可沿用熟悉的工作方式。', 78, 173, 1128, 57, 26, { color: C.silver });
- const rows = [
- ['01', '填写接入地址', '复制控制台提供的 Base URL', 288],
- ['02', '配置 API Key', '创建密钥,并选择所需的分组与额度', 407],
- ['03', '选择 Astra', '按控制台模型名称配置,开始调用', 526],
- ];
- for (const [n, label, detail, y] of rows) {
- text(s, `onboard-number-${n}`, n, 78, y, 140, 75, 52, { color: C.blue });
- text(s, `onboard-label-${n}`, label, 233, y + 6, 450, 62, 34, { bold: true });
- text(s, `onboard-detail-${n}`, detail, 704, y + 9, 500, 54, 25, { color: C.silver });
- }
- notes(s, '项目提供客户端配置辅助。适用于支持自定义Base URL的客户端和SDK。实际地址、密钥和模型标识从控制台复制,密钥权限需要覆盖所选模型分组。这里不公开任何用户密钥,也不填入未确认的站点域名。', [SOURCES.clients, SOURCES.astra, SOURCES.key]);
- }
- // 10. Close on the platform brand, not on a standalone model identity.
- {
- const s = slideBase(10, false);
- art(s, 'cover-halo', 610, 76, 670, 559);
- text(s, 'closing-brand', '聚合AI', 76, 164, 650, 116, 84, { bold: true });
- text(s, 'closing-positioning', 'Astra 主推,多模型聚合', 80, 302, 910, 74, 40);
- text(s, 'closing-promise', '不降智,稳定,高速', 80, 430, 850, 60, 32, { color: C.blue });
- text(s, 'closing-cta', '创建 API Key,连接你的常用工具。', 80, 584, 1060, 51, 27, { color: C.silver });
- notes(s, '总结聚合AI的定位:Astra为主推模型,多模型聚合作为平台价值。用户可以在控制台创建API Key,并按对应协议接入常用工具。', [SOURCES.platform, SOURCES.clients], ['cover-halo']);
- }
- for (const { slide, page } of pendingFolios) {
- text(slide, `folio-${page}`, String(page), 1180, 653, 28, 25, 17, { color: C.muted, align: 'right' });
- }
- const candidatePath = path.join(buildDir, `candidate-${revision}.pptx`);
- await fs.writeFile(path.join(buildDir, `text-metrics-${revision}.json`), JSON.stringify(textChecks, null, 2));
- await (await PresentationFile.exportPptx(presentation)).save(candidatePath);
- const result = await finalizePresentation({
- workspaceDir,
- candidatePath,
- finalPath,
- pythonExecutable,
- integrityValidatorPath: path.join(skillDir, 'container_tools/inspect_presentation_package_integrity.py'),
- layoutValidatorPath: path.join(skillDir, 'container_tools/inspect_presentation_layout_geometry.py'),
- layoutArgs: ['--expected-slide-size-emu', '12192000,6858000', '--validate-bullet-geometry', '--validate-heading-fit'],
- explicitTotalSlideCount: 10,
- requiredNativeTableOwnerSlides: [],
- requiredNativeChartOwnerSlides: [],
- fontPolicy,
- verifyArtifactToolImport: true,
- receiptPath: path.join(buildDir, `validation-${revision}.json`),
- });
- await fs.writeFile(path.join(buildDir, `presentation-${revision}.json`), JSON.stringify(presentation.toProto()));
- await fs.writeFile(path.join(buildDir, `build-result-${revision}.json`), JSON.stringify(result, null, 2));
- console.log(JSON.stringify({ finalPath, candidatePath, renderDir, slideCount: presentation.slides.items.length, font }, null, 2));
- }
- await build();
|