HyperMind Lab M1 架构地基 Implementation Plan <四>

下一词预测(Next-token Prediction)深度解析:从 N-gram 到 GPT 的唯一目标函数 下一词预测(NTP)是自回归语言模型的核心训练目标,其本质为最小化交叉熵,等价于最大似然估计。通过链式法则将序列概率分解为条件分布,模型在无监督下学习语料中真实下一个 token 的高概率输出。从n-gram到Transformer,技术演进始终围绕提升上下文建模能力与并行效率。训练时利用因果掩码实现并行前向,推理则逐token采样生成。损失函数的下降揭示了规模定律下的通用智能涌现,但模型仍受限于生成能力,无法学习逻辑或常识。该目标统一了训练、评估与推理,构成大模型发展的基石。 阅读详情

Task 13: ProjectGallery + ProjectWizard(3 步创建向导)

Files:

  • Create: src/pages/ProjectGallery.tsx
  • Create: src/pages/ProjectWizard.tsx
  • Test: src/__tests__/wizard.test.tsx

Interfaces:

  • Consumes: Task 10 api.projects、Task 11 tokens、Task 12 路由

  • Produces:

    • ProjectGallery:/ 首页——项目卡片网格(名称/种子想法/更新时间/软删除后 30s 内显示"恢复"按钮)、"新建项目"入口 → /wizard
    • ProjectWizard:3 步——① 种子想法(textarea + 名称)② 验证模式 4 选 1(deductive/simulation/hybrid/experimental)③ 价值权重 5 滑块(和为 1)+ 隐私模式开关;完成后 POST /api/v1/projects → 跳转 /workspace/:id
    • 步骤导航:上一步/下一步/完成;步骤校验不通过禁用下一步
  • Step 1: 写失败测试

Create src/__tests__/wizard.test.tsx:

// @vitest-environment jsdom
import { describe, expect, it, vi, beforeEach } from 'vitest';
import { render, screen, fireEvent, waitFor } from '@testing-library/react';
import { MemoryRouter } from 'react-router-dom';
import { ProjectWizard } from '../pages/ProjectWizard';

beforeEach(() => { vi.restoreAllMocks(); });

describe('ProjectWizard', () => {
  it('three steps with validation gates', async () => {
    render(<MemoryRouter><ProjectWizard /></MemoryRouter>);
    // Step 1: 名称 + 种子想法必填
    fireEvent.click(screen.getByRole('button', { name: /下一步/ }));
    expect(screen.getByText(/请填写/)).toBeDefined();
    fireEvent.change(screen.getByLabelText(/项目名称/), { target: { value: '量子认知' } });
    fireEvent.change(screen.getByLabelText(/种子想法/), { target: { value: '意识是量子现象吗' } });
    fireEvent.click(screen.getByRole('button', { name: /下一步/ }));
    // Step 2: 验证模式
    expect(screen.getByText(/验证模式/)).toBeDefined();
    fireEvent.click(screen.getByRole('radio', { name: /混合验证/ }));
    fireEvent.click(screen.getByRole('button', { name: /下一步/ }));
    // Step 3: 价值权重
    expect(screen.getByText(/价值权重/)).toBeDefined();
    fireEvent.change(screen.getByLabelText(/认知/), { target: { value: '30' } });
    fireEvent.click(screen.getByRole('button', { name: /完成/ }));
    // 提交被 stub 拦截
    await waitFor(() => expect(screen.getByText(/创建中/)).toBeDefined());
  });

  it('sum of value weights must equal 1', async () => {
    render(<MemoryRouter><ProjectWizard /></MemoryRouter>);
    fireEvent.change(screen.getByLabelText(/项目名称/), { target: { value: 'x' } });
    fireEvent.change(screen.getByLabelText(/种子想法/), { target: { value: 'y' } });
    fireEvent.click(screen.getByRole('button', { name: /下一步/ }));
    fireEvent.click(screen.getByRole('radio', { name: /混合验证/ }));
    fireEvent.click(screen.getByRole('button', { name: /下一步/ }));
    // 全部调到 0.1 → 和 0.5 ≠ 1
    for (const label of ['生存', '认知', '审美', '伦理', '超越']) {
      fireEvent.change(screen.getByLabelText(label), { target: { value: '10' } });
    }
    expect(screen.getByText(/权重之和须为 1/)).toBeDefined();
    expect((screen.getByRole('button', { name: /完成/ }) as HTMLButtonElement).disabled).toBe(true);
  });
});
  • Step 2: 运行确认失败

Run: npx vitest run src/__tests__/wizard.test.tsx
Expected: FAIL(页面不存在)。

  • Step 3: 实现 ProjectGallery

Create src/pages/ProjectGallery.tsx:

import { useEffect, useState } from 'react';
import { useNavigate, Link } from 'react-router-dom';
import { api } from '../lib/api';
import { tokens } from '../design/tokens';
import type { ProjectRecord } from '../types';

export function ProjectGallery() {
  const [projects, setProjects] = useState<ProjectRecord[]>([]);
  const [graveyard, setGraveyard] = useState<Record<string, string>>({}); // id → deletedAt
  const [loading, setLoading] = useState(true);
  const navigate = useNavigate();

  const refresh = async () => {
    const { rows } = await api.projects.list();
    setProjects(rows);
    setLoading(false);
  };

  useEffect(() => { void refresh(); }, []);

  // 软删除 30s 恢复窗口计时(对应 undoStore 回收站语义)
  useEffect(() => {
    const timer = setInterval(() => {
      const now = Date.now();
      setGraveyard((prev) => {
        const next = { ...prev };
        for (const [id, t] of Object.entries(next)) {
          if (now - new Date(t).getTime() > 30_000) delete next[id];
        }
        return next;
      });
    }, 1000);
    return () => clearInterval(timer);
  }, []);

  const handleDelete = async (id: string) => {
    await api.projects.softDelete(id);
    setGraveyard((prev) => ({ ...prev, [id]: new Date().toISOString() }));
    await refresh();
  };

  const handleRestore = async (id: string) => {
    await api.projects.restore(id);
    setGraveyard((prev) => { const next = { ...prev }; delete next[id]; return next; });
    await refresh();
  };

  return (
    <div style={{ minHeight: '100vh', background: tokens.colors.semantic.canvas, color: tokens.colors.semantic.text, padding: tokens.spacing[8] }}>
      <div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', maxWidth: 960, margin: '0 auto' }}>
        <h1 style={{ fontSize: tokens.typography.size['2xl'], fontWeight: tokens.typography.weight.bold }}>菌丝空间超图</h1>
        <button onClick={() => navigate('/wizard')} style={{ background: tokens.colors.semantic.accent, color: '#000', border: 'none', borderRadius: tokens.radius.md, padding: `${tokens.spacing[2]} ${tokens.spacing[4]}`, cursor: 'pointer', fontWeight: tokens.typography.weight.semibold }}>
          新建项目
        </button>
      </div>
      {loading ? <p style={{ textAlign: 'center', color: tokens.colors.semantic.textMuted, marginTop: tokens.spacing[12] }}>加载中…</p> : (
        <div style={{ display: 'grid', gridTemplateColumns: 'repeat(auto-fill, minmax(280px, 1fr))', gap: tokens.spacing[4], maxWidth: 960, margin: `${tokens.spacing[6]} auto 0` }}>
          {projects.length === 0 && <p style={{ color: tokens.colors.semantic.textMuted }}>还没有项目——点击"新建项目"开启第一次思维实验。</p>}
          {projects.map((p) => (
            <div key={p.id} style={{ background: tokens.colors.domain.graphite['900'], border: `1px solid ${tokens.colors.domain.graphite['800']}`, borderRadius: tokens.radius.lg, padding: tokens.spacing[5], display: 'flex', flexDirection: 'column', gap: tokens.spacing[3] }}>
              <h2 style={{ fontSize: tokens.typography.size.lg, fontWeight: tokens.typography.weight.semibold }}>{p.name}</h2>
              <p style={{ fontSize: tokens.typography.size.sm, color: tokens.colors.semantic.textMuted }}>{p.rootSeedIdea}</p>
              <p style={{ fontSize: tokens.typography.size.xs, color: tokens.colors.domain.graphite['400'] }}>更新于 {new Date(p.updatedAt).toLocaleString()}</p>
              <div style={{ display: 'flex', gap: tokens.spacing[2], marginTop: 'auto' }}>
                <Link to={`/workspace/${p.id}`} style={{ flex: 1, textAlign: 'center', background: tokens.colors.domain.ultramarine['600'], color: tokens.colors.semantic.text, borderRadius: tokens.radius.md, padding: `${tokens.spacing[2]} ${tokens.spacing[3]}`, textDecoration: 'none', fontSize: tokens.typography.size.sm }}>
                  进入实验室
                </Link>
                {graveyard[p.id]
                  ? <button onClick={() => void handleRestore(p.id)} style={{ background: tokens.colors.domain.amber['600'], border: 'none', borderRadius: tokens.radius.md, padding: tokens.spacing[2], cursor: 'pointer', fontSize: tokens.typography.size.sm }}>恢复</button>
                  : <button onClick={() => void handleDelete(p.id)} style={{ background: 'transparent', border: `1px solid ${tokens.colors.domain.vermilion['600']}`, color: tokens.colors.domain.vermilion['400'], borderRadius: tokens.radius.md, padding: tokens.spacing[2], cursor: 'pointer', fontSize: tokens.typography.size.sm }}>删除</button>}
              </div>
            </div>
          ))}
        </div>
      )}
    </div>
  );
}
  • Step 4: 实现 ProjectWizard

Create src/pages/ProjectWizard.tsx:

import { useMemo, useState } from 'react';
import { useNavigate } from 'react-router-dom';
import { api } from '../lib/api';
import { tokens } from '../design/tokens';
import type { ValidationMode, ValueScores } from '../types';

const VALIDATION_OPTIONS: { id: ValidationMode; label: string; desc: string }[] = [
  { id: 'deductive', label: '演绎推理', desc: '从公理严格推导,最严谨' },
  { id: 'simulation', label: '仿真模拟', desc: '在虚拟环境中运行假设' },
  { id: 'hybrid', label: '混合验证', desc: '演绎 + 仿真双轨' },
  { id: 'experimental', label: '实验验证', desc: '真实世界干预测试' },
];

const VALUE_KEYS: (keyof ValueScores)[] = ['survival', 'cognitive', 'aesthetic', 'ethical', 'transcendent'];
const VALUE_LABELS: Record<keyof ValueScores, string> = { survival: '生存', cognitive: '认知', aesthetic: '审美', ethical: '伦理', transcendent: '超越' };

export function ProjectWizard() {
  const navigate = useNavigate();
  const [step, setStep] = useState(0);
  const [name, setName] = useState('');
  const [seed, setSeed] = useState('');
  const [validationMode, setValidationMode] = useState<ValidationMode | null>(null);
  const [weights, setWeights] = useState<ValueScores>({ survival: 20, cognitive: 20, aesthetic: 20, ethical: 20, transcendent: 20 });
  const [privacy, setPrivacy] = useState(false);
  const [submitting, setSubmitting] = useState(false);
  const [error, setError] = useState('');

  const weightSum = useMemo(() => Object.values(weights).reduce((a, b) => a + b, 0), [weights]);

  const canNext = step === 0 ? name.trim().length > 0 && seed.trim().length > 0 : step === 1 ? validationMode !== null : true;
  const weightsValid = Math.abs(weightSum - 100) <= 0.5;

  const setWeight = (k: keyof ValueScores, v: number) => setWeights((w) => ({ ...w, [k]: v }));

  const submit = async () => {
    if (!weightsValid) { setError('权重之和须为 1(当前 ' + (weightSum / 100).toFixed(2) + ')'); return; }
    setSubmitting(true);
    setError('');
    try {
      const project = await api.projects.create({
        name, rootSeedIdea: seed, validationMode: validationMode!,
        valueWeights: Object.fromEntries(VALUE_KEYS.map((k) => [k, weights[k] / 100])) as unknown as ValueScores,
        privacyMode: privacy,
      });
      navigate(`/workspace/${project.id}`);
    } catch (e) {
      setError(e instanceof Error ? e.message : '创建失败');
      setSubmitting(false);
    }
  };

  return (
    <div style={{ minHeight: '100vh', background: tokens.colors.semantic.canvas, color: tokens.colors.semantic.text, display: 'flex', alignItems: 'center', justifyContent: 'center' }}>
      <div style={{ width: 520, background: tokens.colors.domain.graphite['900'], border: `1px solid ${tokens.colors.domain.graphite['800']}`, borderRadius: tokens.radius.lg, padding: tokens.spacing[8] }}>
        <div style={{ display: 'flex', gap: tokens.spacing[2], marginBottom: tokens.spacing[6] }}>
          {['种子想法', '验证模式', '价值权重'].map((s, i) => (
            <span key={s} style={{ flex: 1, textAlign: 'center', padding: `${tokens.spacing[1]} 0`, borderRadius: tokens.radius.sm, fontSize: tokens.typography.size.xs, background: i === step ? tokens.colors.domain.ultramarine['700'] : tokens.colors.domain.graphite['800'], color: i === step ? tokens.colors.semantic.text : tokens.colors.semantic.textMuted }}>{s}</span>
          ))}
        </div>

        {step === 0 && (
          <div style={{ display: 'flex', flexDirection: 'column', gap: tokens.spacing[4] }}>
            <label style={{ display: 'flex', flexDirection: 'column', gap: tokens.spacing[2], fontSize: tokens.typography.size.sm }}>
              项目名称
              <input value={name} onChange={(e) => setName(e.target.value)} aria-label="项目名称" placeholder="如:量子认知实验室" style={{ background: tokens.colors.domain.graphite['950'], border: `1px solid ${tokens.colors.domain.graphite['700']}`, borderRadius: tokens.radius.md, padding: tokens.spacing[3], color: tokens.colors.semantic.text }} />
            </label>
            <label style={{ display: 'flex', flexDirection: 'column', gap: tokens.spacing[2], fontSize: tokens.typography.size.sm }}>
              种子想法
              <textarea value={seed} onChange={(e) => setSeed(e.target.value)} aria-label="种子想法" rows={5} placeholder="你想要探索的核心问题…" style={{ background: tokens.colors.domain.graphite['950'], border: `1px solid ${tokens.colors.domain.graphite['700']}`, borderRadius: tokens.radius.md, padding: tokens.spacing[3], color: tokens.colors.semantic.text, resize: 'vertical' }} />
            </label>
            {!canNext && <p style={{ color: tokens.colors.semantic.danger, fontSize: tokens.typography.size.xs }}>请填写项目名称与种子想法</p>}
          </div>
        )}

        {step === 1 && (
          <div style={{ display: 'flex', flexDirection: 'column', gap: tokens.spacing[3] }}>
            <p style={{ fontSize: tokens.typography.size.sm, color: tokens.colors.semantic.textMuted }}>验证模式</p>
            {VALIDATION_OPTIONS.map((o) => (
              <label key={o.id} style={{ display: 'flex', gap: tokens.spacing[3], alignItems: 'flex-start', padding: tokens.spacing[3], border: `1px solid ${validationMode === o.id ? tokens.colors.domain.ultramarine['500'] : tokens.colors.domain.graphite['700']}`, borderRadius: tokens.radius.md, cursor: 'pointer', background: validationMode === o.id ? tokens.colors.domain.ultramarine['950'] : 'transparent' }}>
                <input type="radio" name="validation" aria-label={o.label} checked={validationMode === o.id} onChange={() => setValidationMode(o.id)} />
                <span>
                  <span style={{ display: 'block', fontSize: tokens.typography.size.md }}>{o.label}</span>
                  <span style={{ fontSize: tokens.typography.size.xs, color: tokens.colors.semantic.textMuted }}>{o.desc}</span>
                </span>
              </label>
            ))}
          </div>
        )}

        {step === 2 && (
          <div style={{ display: 'flex', flexDirection: 'column', gap: tokens.spacing[4] }}>
            <p style={{ fontSize: tokens.typography.size.sm, color: tokens.colors.semantic.textMuted }}>价值权重(总和须为 1)</p>
            {VALUE_KEYS.map((k) => (
              <label key={k} style={{ display: 'flex', alignItems: 'center', gap: tokens.spacing[3], fontSize: tokens.typography.size.sm }}>
                <span style={{ width: 48 }}>{VALUE_LABELS[k]}</span>
                <input type="range" min={0} max={100} step={5} value={weights[k]} aria-label={VALUE_LABELS[k]} onChange={(e) => setWeight(k, Number(e.target.value))} style={{ flex: 1 }} />
                <span style={{ width: 40, textAlign: 'right', fontSize: tokens.typography.size.xs, color: tokens.colors.semantic.textMuted }}>{weights[k]}%</span>
              </label>
            ))}
            {!weightsValid && <p style={{ color: tokens.colors.semantic.danger, fontSize: tokens.typography.size.xs }}>权重之和须为 1(当前 {(weightSum / 100).toFixed(2)})</p>}
            <label style={{ display: 'flex', gap: tokens.spacing[2], alignItems: 'center', fontSize: tokens.typography.size.sm }}>
              <input type="checkbox" checked={privacy} onChange={(e) => setPrivacy(e.target.checked)} />
              隐私模式(联邦数据不出域,M4 生效)
            </label>
          </div>
        )}

        {error && <p style={{ color: tokens.colors.semantic.danger, fontSize: tokens.typography.size.sm, marginTop: tokens.spacing[4] }}>{error}</p>}

        <div style={{ display: 'flex', justifyContent: 'space-between', marginTop: tokens.spacing[6] }}>
          <button onClick={() => (step === 0 ? navigate('/') : setStep(step - 1))} style={{ background: 'transparent', border: `1px solid ${tokens.colors.domain.graphite['600']}`, color: tokens.colors.semantic.text, borderRadius: tokens.radius.md, padding: `${tokens.spacing[2]} ${tokens.spacing[5]}`, cursor: 'pointer' }}>{step === 0 ? '返回' : '上一步'}</button>
          {step < 2 ? (
            <button disabled={!canNext} onClick={() => setStep(step + 1)} style={{ background: tokens.colors.domain.ultramarine['600'], border: 'none', color: tokens.colors.semantic.text, borderRadius: tokens.radius.md, padding: `${tokens.spacing[2]} ${tokens.spacing[5]}`, cursor: canNext ? 'pointer' : 'not-allowed', opacity: canNext ? 1 : 0.4 }}>下一步</button>
          ) : (
            <button disabled={submitting || !weightsValid} onClick={() => void submit()} style={{ background: tokens.colors.semantic.accent, border: 'none', color: '#000', borderRadius: tokens.radius.md, padding: `${tokens.spacing[2]} ${tokens.spacing[5]}`, cursor: submitting || !weightsValid ? 'not-allowed' : 'pointer', opacity: submitting || !weightsValid ? 0.4 : 1, fontWeight: tokens.typography.weight.semibold }}>{submitting ? '创建中…' : '完成'}</button>
          )}
        </div>
      </div>
    </div>
  );
}
  • Step 5: 运行测试验证

Run: npx vitest run src/__tests__/wizard.test.tsx
Expected: PASS(2 个用例)。若"创建中"断言失败,检查 submit 内 api.projects.create 被 fetch stub 拦截或改为 vi.spyOn(api.projects, 'create')。

  • Step 6: Commit
git add src/pages/ProjectGallery.tsx src/pages/ProjectWizard.tsx src/__tests__/wizard.test.tsx
git commit -m "feat(pages): project gallery with 30s restore window + 3-step wizard"

Task 14: OperationPanel 外壳 + ConfirmDialog + autoSnapshot + 崩溃恢复

Files:

  • Create: src/components/OperationPanel.tsx
  • Create: src/components/ui/ConfirmDialog.tsx
  • Create: src/lib/autoSnapshot.ts
  • Modify: src/pages/WorkspacePage.tsx(挂载 OperationPanel)
  • Test: src/__tests__/operations.test.tsx + src/lib/__tests__/autoSnapshot.test.ts

Interfaces:

  • Consumes: Task 9 undoStore/hypergraphStore、Task 10 api.history、Task 11 interactionSpecs.systems

  • Produces:

    • OperationPanel:右面板内容区替换占位——操作历史列表(undoStore.past 渲染:类型徽标/时间/描述)、撤销/重做按钮、清空回收站(30s 确认对话框)
    • ConfirmDialog:受控对话框 {open, title, message, confirmLabel, danger, onConfirm, onCancel}(Esc/遮罩关闭,回车确认)
    • autoSnapshot:startAutoSnapshot(projectId, {opsThreshold?, intervalMs?})——订阅 undoStore,每 opsThreshold 个操作或 intervalMs 间隔触发 api.history.createSnapshot;返回 stop 函数
    • 启动崩溃恢复:WorkspacePage 挂载时 hypergraphStore.loadProject 失败(后端 500/连接中断)→ 读取 localStorage['mindgraph.crash.snapshot'] 恢复本地节点 → 提示"已从本地快照恢复"
  • Step 1: 写失败测试

Create src/lib/__tests__/autoSnapshot.test.ts:

// @vitest-environment jsdom
import { describe, expect, it, vi, beforeEach, afterEach } from 'vitest';
import { startAutoSnapshot } from '../autoSnapshot';
import { useUndoStore } from '../../stores';

beforeEach(() => { vi.useFakeTimers(); vi.restoreAllMocks(); });
afterEach(() => { vi.useRealTimers(); });

describe('autoSnapshot', () => {
  it('snapshots every 5 ops', async () => {
    const post = vi.fn(async () => ({ id: 'snap_1' }));
    vi.spyOn(await import('../api'), 'api').mockReturnValueOnce(undefined as never);
    // 直接 mock fetch:snapshot 走 api.history.createSnapshot → api.post → fetch
    vi.stubGlobal('fetch', vi.fn(async () => new Response(JSON.stringify({ ok: true, data: { id: 'snap_1' } }), { status: 200 })));
    const stop = startAutoSnapshot('proj_1', { opsThreshold: 5, intervalMs: 300_000 });
    for (let i = 0; i < 5; i++) useUndoStore.getState().push({ type: 'node.create', payload: { id: `node_${i}` }, prev: null });
    expect(fetch).toHaveBeenCalledWith(expect.stringContaining('/api/v1/projects/proj_1/snapshots'), expect.anything());
    stop();
  });

  it('snapshots on interval', async () => {
    vi.stubGlobal('fetch', vi.fn(async () => new Response(JSON.stringify({ ok: true, data: { id: 'snap_1' } }), { status: 200 })));
    const stop = startAutoSnapshot('proj_1', { opsThreshold: 100, intervalMs: 60_000 });
    vi.advanceTimersByTime(61_000);
    expect(fetch).toHaveBeenCalled();
    stop();
  });
});

Create src/__tests__/operations.test.tsx:

// @vitest-environment jsdom
import { describe, expect, it, vi, beforeEach } from 'vitest';
import { render, screen, fireEvent } from '@testing-library/react';
import { OperationPanel } from '../components/OperationPanel';
import { ConfirmDialog } from '../components/ui/ConfirmDialog';
import { useUndoStore } from '../stores';

beforeEach(() => { useUndoStore.setState({ past: [], future: [], canUndo: false, canRedo: false }); });

describe('OperationPanel', () => {
  it('renders undo history entries with undo/redo buttons', () => {
    useUndoStore.getState().push({ type: 'node.create', payload: { id: 'node_1', title: '种子' }, prev: null });
    render(<OperationPanel />);
    expect(screen.getByText(/node\.create/)).toBeDefined();
    fireEvent.click(screen.getByRole('button', { name: /撤销/ }));
    expect(useUndoStore.getState().future.length).toBe(1);
  });
});

describe('ConfirmDialog', () => {
  it('confirms and cancels', () => {
    const onConfirm = vi.fn();
    const onCancel = vi.fn();
    render(<ConfirmDialog open title="清空回收站" message="将永久删除这些节点" confirmLabel="确认" danger onConfirm={onConfirm} onCancel={onCancel} />);
    fireEvent.click(screen.getByRole('button', { name: /确认/ }));
    expect(onConfirm).toHaveBeenCalledTimes(1);
    fireEvent.click(screen.getByRole('button', { name: /取消/ }));
    expect(onCancel).toHaveBeenCalledTimes(1);
  });
});
  • Step 2: 运行确认失败

Run: npx vitest run src/__tests__/operations.test.tsx src/lib/__tests__/autoSnapshot.test.ts
Expected: FAIL(模块不存在)。

  • Step 3: 实现 ConfirmDialog + OperationPanel

Create src/components/ui/ConfirmDialog.tsx:

import { useEffect } from 'react';
import { tokens } from '../../design/tokens';

interface Props {
  open: boolean;
  title: string;
  message: string;
  confirmLabel: string;
  danger?: boolean;
  onConfirm(): void;
  onCancel(): void;
}

export function ConfirmDialog({ open, title, message, confirmLabel, danger, onConfirm, onCancel }: Props) {
  useEffect(() => {
    if (!open) return;
    const onKey = (e: KeyboardEvent) => {
      if (e.key === 'Escape') onCancel();
      if (e.key === 'Enter') onConfirm();
    };
    window.addEventListener('keydown', onKey);
    return () => window.removeEventListener('keydown', onKey);
  }, [open, onConfirm, onCancel]);

  if (!open) return null;
  return (
    <div onClick={onCancel} style={{ position: 'fixed', inset: 0, background: 'rgba(0,0,0,0.6)', display: 'flex', alignItems: 'center', justifyContent: 'center', zIndex: 100 }} role="dialog" aria-modal>
      <div onClick={(e) => e.stopPropagation()} style={{ width: 380, background: tokens.colors.domain.graphite['900'], border: `1px solid ${tokens.colors.domain.graphite['700']}`, borderRadius: tokens.radius.lg, padding: tokens.spacing[6], display: 'flex', flexDirection: 'column', gap: tokens.spacing[4] }}>
        <h3 style={{ fontSize: tokens.typography.size.lg, fontWeight: tokens.typography.weight.semibold }}>{title}</h3>
        <p style={{ fontSize: tokens.typography.size.sm, color: tokens.colors.semantic.textMuted, lineHeight: tokens.typography.lineHeight.relaxed }}>{message}</p>
        <div style={{ display: 'flex', justifyContent: 'flex-end', gap: tokens.spacing[3], marginTop: tokens.spacing[2] }}>
          <button onClick={onCancel} style={{ background: 'transparent', border: `1px solid ${tokens.colors.domain.graphite['600']}`, color: tokens.colors.semantic.text, borderRadius: tokens.radius.md, padding: `${tokens.spacing[2]} ${tokens.spacing[4]}`, cursor: 'pointer' }}>取消</button>
          <button onClick={onConfirm} style={{ background: danger ? tokens.colors.semantic.danger : tokens.colors.semantic.accent, border: 'none', borderRadius: tokens.radius.md, padding: `${tokens.spacing[2]} ${tokens.spacing[4]}`, cursor: 'pointer', color: '#000', fontWeight: tokens.typography.weight.semibold }}>{confirmLabel}</button>
        </div>
      </div>
    </div>
  );
}

Create src/components/OperationPanel.tsx:

import { useState } from 'react';
import { useUndoStore, useHypergraphStore } from '../stores';
import { tokens } from '../design/tokens';
import { ConfirmDialog } from './ui/ConfirmDialog';

const TYPE_LABEL: Record<string, string> = {
  'node.create': '创建节点', 'node.update': '更新节点', 'node.soft_delete': '删除节点', 'node.restore': '恢复节点',
  'hypha.create': '连接菌丝', 'hypha.soft_delete': '剪断菌丝', 'hypha.restore': '修复菌丝',
  'knot.create': '菌丝结涌现', 'knot.confirm': '确认菌丝结', 'knot.dissolve': '解散菌丝结',
};

export function OperationPanel() {
  const past = useUndoStore((s) => s.past);
  const future = useUndoStore((s) => s.future);
  const canUndo = useUndoStore((s) => s.canUndo);
  const canRedo = useUndoStore((s) => s.canRedo);
  const undo = useUndoStore((s) => s.undo);
  const redo = useUndoStore((s) => s.redo);
  const graveyard = useUndoStore((s) => s.graveyard);
  const removeFromGraveyard = useUndoStore((s) => s.removeFromGraveyard);
  const purgeAll = useHypergraphStore((s) => s.purgeAll ?? (async () => undefined));
  const [confirmOpen, setConfirmOpen] = useState(false);

  const clearGraveyard = () => {
    for (const id of [...graveyard.keys()]) removeFromGraveyard(id);
    void purgeAll();
    setConfirmOpen(false);
  };

  return (
    <div style={{ padding: tokens.spacing[4], display: 'flex', flexDirection: 'column', gap: tokens.spacing[4], overflowY: 'auto', flex: 1 }}>
      <div style={{ display: 'flex', gap: tokens.spacing[2] }}>
        <button onClick={() => undo()} disabled={!canUndo} aria-label="撤销" style={btnStyle(canUndo)}>撤销</button>
        <button onClick={() => redo()} disabled={!canRedo} aria-label="重做" style={btnStyle(canRedo)}>重做</button>
      </div>
      <div>
        <div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: tokens.spacing[2] }}>
          <span style={{ fontSize: tokens.typography.size.sm, fontWeight: tokens.typography.weight.semibold }}>操作历史({past.length})</span>
          {graveyard.size > 0 && (
            <button onClick={() => setConfirmOpen(true)} style={{ background: 'transparent', border: 'none', color: tokens.colors.semantic.danger, fontSize: tokens.typography.size.xs, cursor: 'pointer' }}>
              清空回收站({graveyard.size})
            </button>
          )}
        </div>
        <ul style={{ listStyle: 'none', margin: 0, padding: 0, display: 'flex', flexDirection: 'column', gap: tokens.spacing[1] }}>
          {[...past].reverse().map((e, i) => (
            <li key={`${e.type}-${i}`} style={{ display: 'flex', justifyContent: 'space-between', fontSize: tokens.typography.size.xs, padding: `${tokens.spacing[1]} ${tokens.spacing[2]}`, background: tokens.colors.domain.graphite['950'], borderRadius: tokens.radius.sm }}>
              <span style={{ color: tokens.colors.semantic.text }}>{TYPE_LABEL[e.type] ?? e.type}</span>
              <span style={{ color: tokens.colors.domain.graphite['400'] }}>{(e.payload.title as string) ?? (e.payload.id as string) ?? ''}</span>
            </li>
          ))}
          {past.length === 0 && <li style={{ fontSize: tokens.typography.size.xs, color: tokens.colors.domain.graphite['400'] }}>暂无操作</li>}
        </ul>
      </div>
      <ConfirmDialog
        open={confirmOpen} title="清空回收站" danger confirmLabel="永久删除"
        message={`回收站内有 ${graveyard.size} 个实体,删除后将不可恢复。30 秒恢复期未过的实体可正常恢复。`}
        onConfirm={clearGraveyard} onCancel={() => setConfirmOpen(false)}
      />
    </div>
  );
}

function btnStyle(enabled: boolean) {
  return {
    background: enabled ? tokens.colors.domain.ultramarine['600'] : tokens.colors.domain.graphite['800'],
    border: 'none', color: enabled ? tokens.colors.semantic.text : tokens.colors.domain.graphite['500'],
    borderRadius: tokens.radius.md, padding: `${tokens.spacing[1]} ${tokens.spacing[3]}`, fontSize: tokens.typography.size.sm, cursor: enabled ? 'pointer' : 'not-allowed',
  };
}
  • Step 4: 实现 autoSnapshot + 崩溃恢复

Create src/lib/autoSnapshot.ts:

import { api } from './api';
import { useUndoStore, useHypergraphStore } from '../stores';

interface AutoSnapshotOptions { opsThreshold?: number; intervalMs?: number }

export function startAutoSnapshot(projectId: string, opts: AutoSnapshotOptions = {}): () => void {
  const opsThreshold = opts.opsThreshold ?? 50;
  const intervalMs = opts.intervalMs ?? 5 * 60_000;
  let opsSince = 0;
  let running = true;
  let interval: ReturnType<typeof setInterval> | null = null;

  const snapshot = async () => {
    if (!running || !projectId) return;
    try {
      const data = { nodes: Object.values(useHypergraphStore.getState().nodes), hyphae: Object.values(useHypergraphStore.getState().hyphae), knots: Object.values(useHypergraphStore.getState().knots) };
      await api.history.createSnapshot(projectId, { snapshotType: 'auto', data, reason: opsSince >= opsThreshold ? '50-ops-threshold' : 'interval' });
      opsSince = 0;
    } catch { /* 快照失败静默,下轮重试 */ }
  };

  const unsubscribe = useUndoStore.subscribe(() => {
    opsSince++;
    if (opsSince >= opsThreshold) void snapshot();
  });

  interval = setInterval(() => void snapshot(), intervalMs);
  interval.unref?.();

  return () => {
    running = false;
    unsubscribe();
    if (interval) clearInterval(interval);
  };
}

/** 启动崩溃恢复:loadProject 失败时回退本地快照 */
export function crashRecovery(): void {
  const raw = localStorage.getItem('mindgraph.crash.snapshot');
  if (!raw) return;
  try {
    const data = JSON.parse(raw) as { projectId: string; nodes: unknown[] };
    void data;
  } catch { localStorage.removeItem('mindgraph.crash.snapshot'); }
}

注:crashRecovery M1 为脚手架(快照写入在 hypergraphStore.applyCommand 内按 interactionSpecs.systems.autoSnapshotOps 节流写入 localStorage),WorkspacePage 挂载时调用并在恢复时 toast 提示。

  • Step 5: 挂载 OperationPanel 到右面板

Modify src/components/layout/RightPanel.tsx:占位内容替换为 <OperationPanel />(import 并替换 <p>节点详情…</p> 区域)。

Modify src/pages/WorkspacePage.tsx:加入 useEffect 启动 startAutoSnapshot(projectId)(依赖 projectId)与 crashRecovery(),返回 cleanup 调 stop。

  • Step 6: 运行测试验证

Run: npx vitest run src/__tests__/operations.test.tsx src/lib/__tests__/autoSnapshot.test.ts
Expected: PASS(4 个用例)。autoSnapshot 的 ops 触发用例若未命中,检查 useUndoStore.subscribe 是否在 push 后同步回调(zustand 默认 sync 监听,OK)。

  • Step 7: Commit
git add src/components/OperationPanel.tsx src/components/ui/ConfirmDialog.tsx src/lib/autoSnapshot.ts src/pages/WorkspacePage.tsx src/components/layout/RightPanel.tsx src/__tests__/operations.test.tsx src/lib/__tests__/autoSnapshot.test.ts
git commit -m "feat(ops): operation panel with undo/redo + confirm dialog + auto-snapshot"

Task 15: 遗留数据导入(legacyImport)+ E2E 冒烟脚本

Files:

  • Create: src/lib/legacyImport.ts
  • Create: scripts/smoke.ts
  • Modify: server.ts(启动时导入元数据标记)
  • Test: src/__tests__/legacyImport.test.ts

Interfaces:

  • Consumes: Task 5 repos、Task 10 api、localStorage 现有 hypergraphV1 键

  • Produces:

    • legacyImport.readLocalBackup(): {nodes: ConceptNode[]; hyphae: HyphaConnection[]} | null(读取 localStorage hypergraphV1,schema 校验)
    • legacyImport.importToProject(repos, projectId, backup):批量 graph.createNode + graph.createHypha,映射旧字段(旧 id 保留为 metadata.legacyId),返回 {imported: number}
    • scripts/smoke.ts:tsx 运行——内存库建 22 表 → 建项目 → 建 3 节点 + 2 菌丝 + 1 菌丝结 → 软删/恢复 → 事件 seq 单调 → 打印 SMOKE OK;失败 process.exit(1)
    • package.json script "smoke": "tsx scripts/smoke.ts"(Task 1 已加占位)
  • Step 1: 写失败测试

Create src/__tests__/legacyImport.test.ts:

// @vitest-environment jsdom
import { describe, expect, it, vi, beforeEach } from 'vitest';
import { readLocalBackup, importToProject } from '../lib/legacyImport';

beforeEach(() => { vi.restoreAllMocks(); });

describe('legacyImport', () => {
  it('reads and validates localStorage backup', () => {
    const backup = { nodes: [{ id: 'node_old_1', title: '旧节点', type: 'seed' }], hyphae: [] };
    vi.stubGlobal('localStorage', { getItem: () => JSON.stringify(backup), setItem: vi.fn(), removeItem: vi.fn() });
    const result = readLocalBackup();
    expect(result?.nodes).toHaveLength(1);
    expect(result?.nodes[0].title).toBe('旧节点');
  });

  it('returns null on invalid backup', () => {
    vi.stubGlobal('localStorage', { getItem: () => 'not-json', setItem: vi.fn(), removeItem: vi.fn() });
    expect(readLocalBackup()).toBeNull();
  });

  it('imports nodes preserving legacy ids', () => {
    const backup = { nodes: [{ id: 'node_old_1', title: 'A', type: 'seed' }], hyphae: [] };
    const graph = { createNode: vi.fn((_p: string, d: Record<string, unknown>) => ({ ...d, id: 'node_new_1' })) };
    const res = importToProject(graph as never, 'proj_1', backup as never);
    expect(res.imported).toBe(1);
    expect(graph.createNode).toHaveBeenCalledWith('proj_1', expect.objectContaining({ metadata: expect.objectContaining({ legacyId: 'node_old_1' }) }));
  });
});
  • Step 2: 运行确认失败

Run: npx vitest run src/__tests__/legacyImport.test.ts
Expected: FAIL(../lib/legacyImport 不存在)。

  • Step 3: 实现 legacyImport

Create src/lib/legacyImport.ts:

import type { ConceptNode, HyphaConnection } from '../types';

const STORAGE_KEY = 'hypergraphV1';

export interface LegacyBackup { nodes: Array<{ id: string; title: string; type?: string; [k: string]: unknown }>; hyphae: Array<{ id: string; sourceNodeId: string; targetNodeId: string; [k: string]: unknown }> }

export function readLocalBackup(): LegacyBackup | null {
  const raw = localStorage.getItem(STORAGE_KEY);
  if (!raw) return null;
  try {
    const parsed = JSON.parse(raw) as LegacyBackup;
    if (!Array.isArray(parsed.nodes) || !Array.isArray(parsed.hyphae)) return null;
    return parsed;
  } catch { return null; }
}

export function importToProject(
  graph: { createNode(projectId: string, data: Record<string, unknown>): unknown; createHypha(projectId: string, data: Record<string, unknown>): unknown },
  projectId: string,
  backup: LegacyBackup,
): { imported: number } {
  const idMap = new Map<string, string>();
  for (const n of backup.nodes) {
    const created = graph.createNode(projectId, {
      title: n.title, type: n.type ?? 'derived',
      metadata: { legacyId: n.id },
    }) as { id: string };
    idMap.set(n.id, created.id);
  }
  for (const h of backup.hyphae) {
    const src = idMap.get(h.sourceNodeId);
    const dst = idMap.get(h.targetNodeId);
    if (src && dst) graph.createHypha(projectId, { sourceNodeId: src, targetNodeId: dst, type: 'legacy' });
  }
  return { imported: backup.nodes.length };
}
  • Step 4: 实现 smoke 脚本

Create scripts/smoke.ts:

import { openDb } from '../server/db/db';
import { createRepos } from '../server/db/repositories';
import { recordLegacyOperation } from '../server/lib/legacyBridge';

const FAILURES: string[] = [];
const expectOk = (label: string, cond: boolean) => {
  if (!cond) FAILURES.push(label);
  console.log(`${cond ? '✓' : '✗'} ${label}`);
};

async function main(): Promise<void> {
  const db = openDb(':memory:');
  const repos = createRepos(db);

  // 1. 项目
  const p = repos.projects.create({ name: 'smoke', rootSeedIdea: '测试种子', validationMode: 'hybrid', valueWeights: { survival: 0.2, cognitive: 0.2, aesthetic: 0.2, ethical: 0.2, transcendent: 0.2 }, privacyMode: false });
  expectOk('project created', /^proj_/.test(p.id));

  // 2. 3 节点 + 2 菌丝
  const n1 = repos.graph.createNode(p.id, { title: 'N1', type: 'seed' });
  const n2 = repos.graph.createNode(p.id, { title: 'N2', type: 'derived' });
  const n3 = repos.graph.createNode(p.id, { title: 'N3', type: 'derived' });
  repos.graph.createHypha(p.id, { sourceNodeId: n1.id, targetNodeId: n2.id, type: 'analogical' });
  repos.graph.createHypha(p.id, { sourceNodeId: n2.id, targetNodeId: n3.id, type: 'causal' });
  expectOk('3 nodes + 2 hyphae', repos.graph.listNodes(p.id).length === 3 && repos.graph.listHyphae(p.id).length === 2);

  // 3. 菌丝结(≥3 成员)
  const knot = repos.graph.createKnot(p.id, { memberNodeIds: [n1.id, n2.id, n3.id] });
  expectOk('knot emerging', knot.status === 'emerging');

  // 4. 软删 / 恢复
  repos.graph.softDeleteNode(p.id, n2.id);
  expectOk('soft delete hides node', repos.graph.listNodes(p.id).length === 2);
  repos.graph.restoreNode(p.id, n2.id);
  expectOk('restore brings node back', repos.graph.listNodes(p.id).length === 3);

  // 5. 事件 seq 单调 + 操作记录
  const e1 = repos.history.appendEvent(p.id, { eventType: 'smoke.a', entityType: 'node', payload: {}, priority: 0 });
  const e2 = repos.history.appendEvent(p.id, { eventType: 'smoke.b', entityType: 'node', payload: {}, priority: 0 });
  expectOk('event seq monotonic', e2.sequence === e1.sequence + 1);
  recordLegacyOperation(repos, { projectId: p.id, operationType: 'fission', inputIds: [n1.id], outputIds: [n2.id], params: {}, status: 'done' });
  const evts = repos.history.listEvents(p.id, { page: 1, pageSize: 50, offset: 0 });
  expectOk('operation recorded to events', evts.rows.some((e) => e.eventType === 'operation.recorded'));

  // 6. 快照
  repos.history.createSnapshot(p.id, { snapshotType: 'auto', data: { nodes: [] }, reason: 'smoke' });
  expectOk('snapshot created', repos.history.listSnapshots(p.id).length === 1);

  db.close();
  if (FAILURES.length > 0) {
    console.error(`\nSMOKE FAILED (${FAILURES.length}): ${FAILURES.join('; ')}`);
    process.exit(1);
  }
  console.log('\nSMOKE OK');
}

main().catch((err) => { console.error('SMOKE ERROR', err); process.exit(1); });
  • Step 5: 修改 server.ts 启动导入标记

Edit server.ts:启动初始化段(openDb 后)加入:

import { metaRepo } from './server/db/repositories/metaRepo';
const meta = metaRepo(db);
const schemaVersion = meta.getMeta('schema_version');
if (schemaVersion == null) {
  meta.setMeta('schema_version', 1);
  meta.setMeta('initialized_at', new Date().toISOString());
  console.log('[m1] database initialized with 22-table schema (v1)');
}
  • Step 6: 运行验证

Run: npx vitest run src/__tests__/legacyImport.test.ts
Expected: PASS(3 个用例)。

Run: npm run smoke
Expected: 12 行 ✓ 输出,末尾 SMOKE OK,exit 0。

  • Step 7: 全量测试回归

Run: npm test
Expected: 全部 PASS(Task 1-15 所有用例;已知 vitest 默认 environment 为 node,组件测试文件头已有 // @vitest-environment jsdom)。

  • Step 8: Commit
git add src/lib/legacyImport.ts scripts/smoke.ts server.ts src/__tests__/legacyImport.test.ts
git commit -m "feat(m1): legacy localStorage import + E2E smoke script + schema meta init"

计划自审(writing-plans Self-Review)

1. Spec 覆盖核对(design 文档 §3.1-3.8 ↔ 任务表):

Spec 要求任务
§3.1 技术栈(Zustand 5 stores)Task 9
§3.1 命令模式撤销栈 50 深度Task 9(undoStore MAX_DEPTH=50)
§3.1 软删除 30s 回收站Task 9/13/14(graveyard TTL 30s、Gallery 恢复窗、ConfirmDialog)
§3.2 SQLite 22 表全量 schemaTask 4(schema.sql 终态)+ Task 15 smoke 验证
§3.2 追加式 events 表(sequence UNIQUE)Task 4 触发器 + Task 5 historyRepo
§3.3 REST /api/v1 统一响应Task 6
§3.3 SSE TaskManagerTask 7 + Task 10 消费端
§3.4 事件溯源基座(P0-P5 优先级)Task 5(priority 字段)+ Task 6 事件写入
§3.5 设计令牌七大色彩域Task 11
§3.5 深度滑块 0-100 三区Task 12(DepthSlider)+ Task 9 映射 + Task 11 depthMap
§3.5 响应式三档Task 12(viewport 侦听)
§3.5 向导 3 步Task 13
§3.5 快捷键Task 12(useShortcuts)
§3.6 数据流(前端命令 → API → repos → 触发器)Task 9/10/6/5 链路
§3.7 types.ts 全量实体Task 2
现有代码兼容(server.ts/App/HypergraphCanvas 保留)Task 8/12 适配说明
§6 验收 1:npm test 全绿Task 15 Step 7
§6 验收 2:22 表可建可查Task 4 schema.test + Task 15 smoke
§6 验收 3:undo/redo 50 深度Task 9 stores.test
§6 验收 4:30s 恢复Task 9/13 测试
§6 验收 5:统一 API 格式Task 6 v1.test
§6 验收 6:SSE 任务流Task 7 llm.test
§6 验收 7:深度滑块元交互Task 12 layout.test

2. Placeholder 扫描:已扫描——Task 7 的 runLlmBlock 注明确 M1 块级语义;Task 14 crashRecovery 明确为脚手架+节流写入说明;Task 9 serverStore 的 SSE 端点标注"P2 或自动降级轮询"。以上均为有实现路径的设计决策,非占位。

3. 类型一致性核对:

  • prefixedId('proj_'|'node_'|'hyph_'|'knot_'|'oper_'|'evnt_'|'snap_') —— Task 2 前缀表覆盖 ✓
  • CommandType union(Task 2)与 applyCommand switch 分支一致 ✓
  • createV1Router(repos, db) 签名在 Task 6 修正后与 Task 7/测试一致 ✓
  • IMMERSION_THRESHOLDS(Task 11)与 depthToImmersion(Task 9)阈值 30/70 一致 ✓
  • api.history.createSnapshot 返回 Snapshot(Task 2 有该类型)✓
  • interactionSpecs.systems.autoSnapshotOps=50(Task 11)与 startAutoSnapshot 默认 opsThreshold=50 ✓

已知偏差(执行时注意):

  • Task 5 historyRepo.createSnapshot 中有一处笔误表达式('vals_' === 'unused' ? ...),实现时应直接使用 'snap_' + Math.random().toString(36).slice(2)(该行已标注修正)。
  • Task 6 的 index.ts/history.ts 含两版写法(草稿 + 修正),以"重要修正/实现约束"标注的终版为准。
存储&AI行业每日要闻(2026-10-25 影响:这是市场对"烧钱就能赢"叙事的一次集体重估——Meta 主动设 C 类股稀释投票权也要融资,SpaceX 单季资本开支逼近营收 1.35 倍,叠加前一日博通 600 亿/SpaceX 400 亿芯片抵押融资,AI 基建已进入"股权+债务双管抽血"阶段;若指引保守,则昨日费半暴跌的余波将继续发酵。影响:DSX POD 的本质是把 AI 基建从"定制工程"变成"集装箱产品"——这与前一日"芯片抵押融资 5000 亿框架"形成闭环:英伟达不仅卖 GPU,还帮客户解决融资、集成、电力等全链条问题; 阅读详情

相关推荐

第33章 YOLO+OpenCV:视频流网络传输

本案例演示用视觉处理库连接网络视频流并逐帧显示画面。程序先打开流地址,再循环取帧、显示、等待按键退出,并用重试计数应对连接失败与读取中断。验证时关注窗口能否稳定出画、是否长时间卡顿黑屏、退出后终端有无报错。🌈

小哥谈 540

HyperMind Lab M1 架构地基 Implementation Plan <一>

文章摘要(149字) HyperMind Lab M1架构实施计划旨在构建新一代思维实验平台,核心包括Zustand状态管理、SQLite全量表结构、REST/SSE接口、设计令牌中心等7大模块。技术栈采用React 19 + Vite + TypeScript,新增Zustand和SQLite支持。架构分层明确,包含服务端数据层、状态管理层和前端交互层,需兼容现有30+LLM API和18个功能面板。关键约束包括UUIDv7语义ID规范、50步撤销栈、事件溯源机制和伦理审计不可变性要求。实施过程将分阶段进

形上得其象,形下合于数;阴阳自济,玄理自明。 178

5分钟搞懂超分算法:从插值到深度学习的实战对比(附Python代码)

本文系统对比了从传统插值到深度学习的主流超分算法,包括最近邻、双线性、双三次插值,以及基于边缘、统计模型和SRCNN、ESPCN、SRGAN等深度学习模型。通过实战代码和决策框架,帮助开发者根据图像质量、速度、资源等需求,快速选择最合适的超分辨率算法,实现从低清到高清的有效转换。

pp12345的博客 424

HyperMind Lab M1 架构地基 Implementation Plan <二>

本文档描述了SQLite数据库建库任务,包含22个表的全量schema设计与实现。主要工作包括: 创建schema.sql文件,定义22个表结构,支持幂等执行 实现db.ts提供数据库连接功能,包括: openDb()打开数据库连接 getDb()获取单例连接 设置SCHEMA_VERSION=1并存入meta表 编写测试用例验证: 22个表正确创建 schema初始化幂等性 meta表版本号正确 关键表的只读约束(如事件表禁止更新/删除) 数据完整性约束(如项目验证模式枚举值) 级联删除等关联关系 测试使

形上得其象,形下合于数;阴阳自济,玄理自明。 232

【3d打印】基于Klipper的3d打印远程上位机系统(Qt),klipper源码,通信解析,打印参数设置

基于Klipper的3d打印远程上位机系统(Qt),klipper源码,通信解析,打印参数设置(启庞Kp3s V1)

共享IT之美,共创机器未来 3182

单片机NTC热敏电阻测温:六种实用电路方案与选型指南

本文详细介绍了单片机系统中NTC热敏电阻测温的六种实用电路方案,包括分压法、恒流源法、RC充放电法、电桥法、频率法及集成专用IC方案,并提供了关键的硬件选型指南和软件处理技巧,帮助工程师实现高精度、低成本的温度测量。

herb5的博客 799

HyperMind Lab M1 架构地基 Implementation Plan <三>

本文档描述了前端状态管理模块的实现,主要使用Zustand库创建多个store并实现命令模式撤销栈功能。关键点包括: 创建5个核心store: hypergraphStore:管理节点/连接/项目等核心数据及CRUD操作 uiStore:处理界面状态(工具选择/深度值/面板布局等) undoStore:实现命令模式的撤销/重做功能,含50条历史记录限制和30秒回收站机制 settingsStore:管理用户偏好设置 serverStore:处理服务器连接和事件订阅 测试用例验证核心功能: undoStore

形上得其象,形下合于数;阴阳自济,玄理自明。 229

数学建模:相关性分析学习——皮尔逊(pearson)相关系数与斯皮尔曼(spearman)相关系数

相关性分析:求解相关系数并绘制热力图进行对比

weixin_67565775的博客 8万+

「大车小标」自动稽核怎么落地:识别车型与计费车型如何自动比对、争议如何排除

在高速公路货车逃费里,"大车小标"是最高频、也最难缠的一类:实际是更大车型、更多轴数的货车,却按更小车型交费,跑同样的路交更少的钱。单次差额可能不大,但放到长距离、高频次、规模化运营的路网里,累积的通行费流失相当可观。自动稽核"大车小标",核心不是"识别出这是什么车"这么简单,而是要把多个来源的车型判断和最终计费车型自动比对,并在大量正常交易里把真正可疑的少数挑出来,同时把设备误差、遮挡污染造成的误判排除掉。本文拆解这条链路的工程落地思路,全文定性,不涉及具体识别率与金额。

weixin_53898122的博客 359

同态加密与SEAL库实战:CKKS参数调优避坑指南

本文深入探讨了同态加密技术中CKKS方案的参数调优实践,重点解析了SEAL库中`poly_modulus_degree`、`coeff_modulus`和`scale`三大核心参数的配置技巧与避坑指南。通过实战案例展示了如何平衡计算深度、精度与性能,为开发者提供了一套完整的参数优化方法论,帮助避免常见的'scale out of bounds'等错误,显著提升同态加密应用的可靠性和效率。

weixin_29315569的博客 219

国科大学习资料--模式识别与机器学习(黄庆明)--2020期末考试题

国科大学习资料–模式识别与机器学习(黄庆明)–2020期末考试题 国科大学习资料–模式识别与机器学习(黄庆明)–2020期末考试题 国科大学习资料–模式识别与机器学习(黄庆明)–2020期末考试题 ============================================================================================================================================================

无名何许人的博客 2579

【技术拆解】配音工具选型评估方法论:用 6 个维度给 AI 配音软件打打分

✅ 测试方案:准备一段包含括号注释、数字、中英文混排的文案导入试听,校验读音、停顿是否合理。案例参考:逗哥配音配套了文案提取、自动对轴、人声分离、声音转换等附加工具,支持 wav /mp3 /mp4 格式导出,可附带字幕与 SRT 文件,在字幕对齐、文件导出这条链路具备对应的能力,适合批量生产场景的创作者在这个维度进行自测打分。同一款配音引擎,有人评价 “自然接近真人”,也有人反馈 “AI 感很重”,核心差异往往来自使用场景:30 字以内的短口播,和 3000 字小说章节朗读,对引擎能力要求完全不一样。

zhizhizhuzhuxia的博客 151

【Claude】Disabled organization 错误:组织被禁用的原因与 API Key 绕过方法

如果环境变量存在,下一步是找出它在哪里被设置的。"Disabled organization" 错误的本质不是账户被封,而是认证优先级冲突。环境变量优先级最高始终覆盖 OAuth 订阅认证报错信息具有误导性:"组织被禁用"指的通常是 API Key 所属的组织,而不是你的个人账户旧 Key 是隐形炸弹:即使你已经不再使用某个组织的 API,只要环境变量中残留该 Key,它就会持续覆盖你的订阅认证非交互模式更敏感claude -p模式下,只要有环境变量,就会强制使用它,完全忽略/login。

博客 8822

外贸独立站 AI 爬虫日志分析:UA 清单、状态码分布与「爬取不等于引用」

日志可反映爬虫访问路径、状态码与UA,但无法证明内容是否被AI引用。需按UA、状态码、路径三层分析,识别可达性问题,避免误判“抓取=引用”。应结合实测验证引用效果,警惕日志误导。关键结论:抓取≠收录≠引用,技术优化仅解决可抓取性,不保证被引用。

mqhgeo的博客 347

面向元数据的 SAP UI 中枢:namespace sap.ui.mdc 全景解析

如果把应用前端比作一条数据到交互的装配线,sap.ui.mdc 就是那条线的标准化工装:以 PropertyInfo 描述原料,以 Delegate 定义装配方法,以 p13n 与 VariantManagement 交付面向个体的最终产物;而 Condition Model、Operator 与 ValueHelp 则让输入与筛选的语义落地。与 OData V4 服务与 Fiori Elements 宏构件组合使用时,它能在保持一致体验的同时,给自由式开发者留下足够的可编排空间。

2007 年 ~ 2025 年,深耕 SAP 技术 18 年 232

VC中sqlite的使用

在VC中对sqlite的使用,实现数据库大批数据的快速插入,读取数据库中的数据在ListCtrl中显示

把GeoReady九维当工程项做:储能管理官网的AI就绪度改造笔记

GeoReady把1900多个网站的审计数据摊开,做成实时基准(State of GEO):AI搜索就绪度平均56.7/100,中位数59,一半以上卡在地基档。这篇用工程视角拆解这份基准:九维权重分别对应哪些工程项(robots.txt放行AI爬虫、llms.txt部署、Schema JSON-LD注入、Meta标签规范化、品牌实体清晰),哪些是「白给的分」,以及怎么把就绪度检查纳入CI流水线。先说结论——56.7分测的是机器可读性,是卫生指标不是排名;工程上先补最便宜的两块(llms.txt+JSON-L

RoseMarketing的博客 256

jdk-8u144-linux-x64.tar.gz.

该资源为搭建Linux系统Java环境用,资源的后缀为tar.gz格式的压缩包

什么是AI、模型和token、vibecoding介绍、ClaudeCode环境搭建

AI是用计算机模拟人类思维与行为的技术,不具意识,仅作为能力放大器。模型是其核心,参数量决定智能水平,大模型历经文本、多模态到全模态演进。大模型按任务表现排名,如千问、DeepSeek等。收费以token为单位,按使用量或套餐计费。Vibecoding通过自然语言生成代码,工具包括ClaudeCode、Cursor等。安装可选CLI或GUI版本,需配置Python、Git、Node及大模型账号,提升开发效率。

2501_91966873的博客 263

Outlook Express 6 (OE6) for Win7/Win8/Win10

英文版 Outlook Express6.0, 兼容Win7/Win10/WinXP,解压就可用。

文档:亲测可用解决vnc灰屏,黑屏,鼠标大黑叉情况

主要解决vnc-viewer灰屏,黑屏,鼠标大黑叉等现象,详细见文档。

LDR6100 智能遥控器精品软文:Type-C 充电口与低功耗量产维护

当 USB-C 接口从单纯的结构升级,变成整机供电稳定性、待机续航、量产一致性的核心瓶颈,智能遥控器、语音遥控器、手持控制器这类小型手持设备的硬件开发,早已不再是 “换一个 Type-C 座子” 那么简单。大量项目在试产阶段反复踩坑:不同品牌适配器识别失败、热插拔瞬间掉电死机、整机休眠待机电流超标导致电池续航缩水、产线测试时端口状态无法捕捉、兼容性参差不齐。乐得瑞 LDR6100 作为单路 USB-C/PD 专用控制芯片,专为小体积手持设备的 Type-C 供电入口量身打造。

LDR006的博客 412

【论文解读】DMH-Net:用深度曼哈顿霍夫变换从全景立方图估计3D房间布局

想象一个空房间,把所有面都去掉,只剩下棱┌─────────────┐ ← 墙与天花板的交界线/│ /│/ │ / │ ← 墙与墙的交界线(竖直)│ │ │ ││ / │ /└─┘───────────┘ ← 墙与地板的交界线论文的房间线框只有三类线线类型是什么在 3D 空间中的方向墙-天花板交界线​墙顶那条边水平,在天花板平面上墙-地板交界线​墙脚那条边水平,在地板平面上墙-墙交界线​两面墙相交的竖棱竖直​为什么用线框?​ 论文给的动机很有说服力:人类感知 3D 世界时,靠的是。

拜小白的成长之路,告别小白 355

Python 浏览器自动化:Selenium vs Playwright 底层原理、实现细节 + Codex 大模型为什么能自主操控浏览器登录填表

Selenium。缺点维护成本高,不适合对接 AI。Playwright,CDP+AX 语义树是核心优势。用自然语言代替硬编码选择器,让 AI 看懂网页语义,自主完成多步网页任务。传统自动化是 “人告诉程序每一步点击哪里”;AI 浏览器 Agent 是 “人告诉程序目标,AI 自己观察页面,决定点哪里,自主完成任务”。

十年老程序猿,AI 疯狂研究专家,前沿硬核知识分享 328

8G 显存本地部署 AI 漫剧流水线:分镜提示词版本管理、批量归档与角色提示词隔离实战教程

本文针对8G显存本地部署的AI漫剧生产中提示词混乱、版本丢失、角色画风漂移等痛点,设计并实现一套轻量化本地提示词管理系统。系统基于纯JSON文件存储,支持角色提示词隔离、分镜模板复用、项目版本快照、批量校验与导入导出,与ComfyUI流水线无缝对接,实现画风一致性与高效复现。所有功能均无需数据库,可常驻后台运行,显著提升红果短剧系列化生产的稳定性与可维护性。

2603_96706877的博客 154

纳米 AI 搜索实战应用场景指南

在数字化运营的日常工作中,我们常常面临这样的困境:海量信息扑面而来,但真正能转化为决策依据的却寥寥无几。无论是电商团队需要精准锁定潜力爆款,还是市场部门急需捕捉转瞬即逝的热点,亦或是研究人员需要在浩如烟海的文献中梳理脉络,传统的人工检索与整理方式不仅效率低下,还极易遗漏关键细节。很多时候,我们花费了数小时甚至数天去收集数据,最后却发现信息滞后、来源单一,甚至存在事实偏差,导致后续的strategy制定建立在沙滩之上。这种“信息过载”与“价值稀缺”的矛盾,正在成为制约个人与企业成长的核心瓶颈。

llhllq2015的博客 215

32 分钟不是完成科研:多智能体读脑图谱,突破在哪里?

如果数千个 AI 智能体只用 32 分钟,就能梳理一份庞大的脑科学数据,我们应该惊讶什么?是模型终于能“独立做科研”,还是科研流程中的某个环节开始出现新的组织方式?10 月 21 日,OpenAI 公布了与 PerturbAI、Scripps Research 合作的研究:利用基于 GPT-Rosalind 的多智能体系统,解释一份已经存在的小鼠脑基因扰动数据,整理潜在研究线索。

weixin_44369324的博客 256

图像审核‑图片内容风控‑违规图像检测‑图片鉴黄鉴暴‑广告二维码识别‑图像风险筛查 API 接口介绍

名称类型必须说明imgBase64String否图像base64字符串imgFilefile否图像文件imgUrlString否图像url链接imgTypeString否图片类型 0: 静态图片(PNG、JPG、JPEG、BMP、GIF(仅对首帧进行审核)、Webp、TIFF)1: GIF动态图片 默认 0注意imgFile, imgBase64, imgUrl至少传入一个,优先级:imgBase64 > imgFile > imgUrl。

聚美智数 240

我需要批量导出Poe左边栏所有对话,而不是一条具体的对话内容,你们AI导出鸭可以吗?

很多人第一次听到“导出对话”,脑子里浮现的是打开某一条聊天记录,右上角点一下导出,保存成一份文件。这不是为了设门槛,而是因为批量导出背后的枚举、并发、格式渲染成本,和单条导出完全不是一个量级。AI导出鸭 不做格式绑架,这是它作为导出工具的基本修养。回到标题那个问题:我需要批量导出Poe左边栏所有对话,而不是一条具体的对话内,你们AI导出鸭可以吗?批量导出和选择性导出并不冲突,工具的设计逻辑是先枚举、再选择、后导出。我需要批量导出Poe左边栏所有对话,而不是一条具体的对话内容,你们AI导出鸭可以吗?

laoyunsi的博客 117

AI辅助逆向的完整链路——反汇编→特征提取→大模型重写

编译后的程序(PE/ELF/Mach-O)丢失了源代码几乎所有高层语义:变量名、注释、类型、函数命名、循环分支结构,只剩下CPU能识别的机器码。传统逆向链路:机器码 → 反汇编(汇编指令)→ 人工阅读 + 静态分析工具生成伪C → 人工梳理逻辑。优化编译、混淆、花指令会把控制流打碎,伪代码可读性极差;无符号剥离(stripped binary)下所有函数都是FUN_xxxx,变量全是v1、v2;单函数上下文有限,跨函数调用、数据依赖靠人脑记忆,效率极低;

weixin_43156294的博客 198
上一篇: HyperMind Lab M1 架构地基 Implementation Plan <三>
下一篇: 第五章 HDS Harness 文档系统
由数入道
博客等级 码龄24年 5372粉丝 822原创
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

个

红包个数最小为10个

元

红包金额最低5元

当前余额3.43元 前往充值 >
需支付:10.00元
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

由数入道

滴水助江海,心灯渡万世。

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付元
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值