Uh oh!
There was an error while loading. Please reload this page.
') + ')', 'gi'); if (regex.test(text)) { found = true; var frag = document.createDocumentFragment(); var parts = text.split(regex); parts.forEach(function(part, i) { if (i % 2 === 0) { frag.appendChild(document.createTextNode(part)); } else { var span = document.createElement('span'); span.className = 'userscript-highlight'; span.textContent = part; frag.appendChild(span); } }); node.parentNode.replaceChild(frag, node); } }); } else if (node.nodeType === 1 && node.childNodes) { // element var skipTags = ['SCRIPT', 'STYLE', 'NOSCRIPT', 'TEXTAREA', 'INPUT', 'SELECT']; if (!skipTags.includes(node.tagName)) { Array.from(node.childNodes).forEach(highlight); } } } highlight(document.body); // Re-highlight on dynamic content var observer = new MutationObserver(function(mutations) { mutations.forEach(function(m) { m.addedNodes.forEach(function(node) { if (node.nodeType === 1 || node.nodeType === 3) highlight(node); }); }); }); observer.observe(document.body, { childList: true, subtree: true }); })(); } } catch(__e) { console.warn('[Userscript:Highlight Search Terms]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + ', 'i'); if (__m === '*' || __re.test(location.href)) { // Strip utm_, fbclid, gclid, etc. from all links on page (function() { var trackingParams = ['utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content', 'fbclid', 'gclid', 'dclid', 'msclkid', 'yclid', 'ref', 'ref_src', 'source', 'medium', 'campaign']; function cleanUrl(url) { try { var u = new URL(url, window.location.origin); var changed = false; trackingParams.forEach(function(p) { if (u.searchParams.has(p)) { u.searchParams.delete(p); changed = true; } }); return changed ? u.toString() : url; } catch (e) { return url; } } function cleanLinks() { document.querySelectorAll('a[href]').forEach(function(a) { var clean = cleanUrl(a.href); if (clean !== a.href) a.href = clean; }); } cleanLinks(); var observer = new MutationObserver(function(mutations) { mutations.forEach(function(m) { m.addedNodes.forEach(function(node) { if (node.nodeType === 1) { if (node.tagName === 'A') cleanLinks(); node.querySelectorAll('a[href]').forEach(function(a) { var clean = cleanUrl(a.href); if (clean !== a.href) a.href = clean; }); } }); }); }); observer.observe(document.body, { childList: true, subtree: true }); })(); } } catch(__e) { console.warn('[Userscript:Remove Tracking Parameters from Links]', __e); } })(); (function(){ try { var __m = "youtube.com"; var __re = new RegExp('^' + "youtube\\.com" + ', 'i'); if (__m === '*' || __re.test(location.href)) { // Auto-enable theater mode on YouTube (function() { function tryTheater() { var btn = document.querySelector('button[aria-label="Theater mode"], ytd-player #player button[title="Theater mode"]'); if (btn && !btn.classList.contains('activated')) { btn.click(); } } // Try immediately tryTheater(); // Try after navigation (SPA) var lastUrl = location.href; setInterval(function() { if (location.href !== lastUrl) { lastUrl = location.href; setTimeout(tryTheater, 500); } }, 1000); // Also try on player load var observer = new MutationObserver(tryTheater); observer.observe(document.body, { childList: true, subtree: true }); })(); } } catch(__e) { console.warn('[Userscript:YouTube Theater Mode Default]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + ', 'i'); if (__m === '*' || __re.test(location.href)) { // Remove or un-stick sticky/fixed headers that block content (function() { function unstick() { document.querySelectorAll('header, nav, [role="banner"], .header, .navbar, .sticky, .fixed-top, [style*="position: fixed"], [style*="position:sticky"]').forEach(function(el) { if (el.style.position === 'fixed' || el.style.position === 'sticky' || getComputedStyle(el).position === 'fixed' || getComputedStyle(el).position === 'sticky') { el.style.position = 'static'; el.style.top = 'auto'; el.style.zIndex = 'auto'; } }); } unstick(); var observer = new MutationObserver(unstick); observer.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ['style', 'class'] }); })(); } } catch(__e) { console.warn('[Userscript:Kill Sticky Headers]', __e); } })(); })();
There was an error while loading. Please reload this page.
参赛项目名称
妙笔(Writer Assistant)· 人人可用的 AI 写作工作台
妙笔是一个 AI 写作工作台。它把 AI 和 Skill 的能力融入写作工作流,真正用 AI 帮用户提升写作产出率。
在流程中,AI 找新闻、提供选题、生成初稿和内容诊断;用户始终负责方向、事实、判断和最终表达。
团队 / 作者
个人参赛
GitHub:@hotcoffeeshake
产品设计、前后端开发、AI Pipeline、Skill 编排与阿里云部署由个人完成。
30 秒看懂妙笔
普通人用 AI 写作往往停留在说一句提示词,最终只有一篇通顺但缺少个人声音的机器稿。
妙笔先给用户提供想看的新闻和爆款内容,再从中提炼标题公式:
演示视频
▶ 点击播放演示视频(约 1 分 37 秒,8.1 MB)
为什么我要做它?
很多做内容获客的朋友跟我讲,一篇内容从想选题到写完要花两三个小时,甚至更久。
他们知道 AI 能帮上忙,但试过之后又觉得写出来的东西机械、死板、缺乏深度,改起来比自己写还累。
问题在于,AI 不知道你是谁、你的读者是谁,也不知道你的写作意图。想让 AI 写出符合我们想法的内容,需要的是 AI、科学工作流和专用 Skill 的组合。
也就是给 AI 套上缰绳,让作者骑着这匹骏马,文思如涌。
妙笔想解决两个问题:
我的原则是:把 AI 做成更高级的笔,帮助用户写作,但不代替用户表达。
百炼在这个项目里做了什么?
百炼不仅参与开发,也是妙笔线上写作和封面生成链路的一部分。
为了让更多评审和真实用户能直接用上妙笔,项目新增了基于邮箱验证码的登录方式。注册与登录验证码通过阿里云能力下发到用户邮箱,用户凭「邮箱 + 验证码」即可完成认证,无需第三方账号就能创建独立账号。
每一条写作数据在落库前先绑定当前账号,不同用户之间互不可见,登录态由 NextAuth 统一管理。这让妙笔从「单机演示」变成了「多用户可用的真实产品」。
同时建议把 技术实现 里的:
鉴权:NextAuth
改为:鉴权:NextAuth + 阿里云邮箱验证码登录
文本能力
项目通过 DashScope OpenAI Compatible API 调用百炼上的 DeepSeek-V3,用于:
AI 网关统一处理用户级调用、超时、限流、积分预检和真实 Token 用量;调用失败时不扣除用户积分。
自动配封面
项目通过阿里云百炼 / DashScope 调用通义万相
wanx2.1-t2i-turbo,结合 Skill 市场的illustration-generator,根据文章内容、标题和发布平台生成不同尺寸、不同风格的封面配图,把找图、裁比例、适配平台这些机械的收尾工作交给 AI,用户只需决定审美与比例。支持纽约客、极简、水彩等风格。用户决定平台、比例和审美方向,AI 负责完成生成与适配。
Skill 如何进入真实写作流程
妙笔没有把 Skill 做成与文章分离的工具箱,而是把不同方法放进最需要它们的阶段:
它们共同服务于同一条文章生命周期,而不是让用户每次重新复制正文、重新解释上下文。
技术实现
wanx2.1-t2i-turbo核心数据包括素材、选题、文章、修改版本、发布记录、Skill 运行记录和用户写作偏好。不同用户的数据按账号隔离,文章修改与阶段推进均落库保存。
效果展示
① 素材浏览:一个入口查询全网热点,激发写作灵感
AI 会抓取全网热点信源,用户也可以导入自己的素材,用新闻和爆款内容激发写作灵感。
② 选题判断:AI 给候选,用户拍板
妙笔可以从全网抓取到的爆款内容里提炼开头写法和标题公式,生成一批带证据的选题,但决定要不要写的还是人。
③ 写作成稿:快速模式与深度访谈
方向明确时快速成稿;上下文不足时先进行多轮访谈,把用户自己的经历、立场和语气带入文章。
④ 初改稿:诊断与 Diff
网上有许多创作者分享自己的写作方法。妙笔精选了一套用于诊断内容与开头的 Skill,写完后可以从读者和 DBS 视角,指出文章在表达效率、认知落差、场景和可信度等方面的不足。
⑤ 审核:发布前最后确认
通过审核清单和诊断摘要完成最终确认,避免文章在事实、结构和表达上带着明显问题进入发布阶段。
⑥ 发布:正文归档与 AI 封面
在发布环节,妙笔还能根据发布平台生成封面配图。找图、裁比例、适配各平台,是最机械、最磨人的收尾工作。用户定下审美和平台,剩下的交给 AI。
⑦ 复盘:让每篇文章成为下一篇的起点
系统对比成稿前后的变化,提取本次创作中得到确认的偏好,沉淀为长期 Writing Profile。
当前部署状态与项目链接
推荐评审体验路径
踩坑记录
1. UI 不应在生产环境里反复试错
早期直接在生产页面上持续调整 UI,导致部署频繁、反馈周期变长。后来改为先在设计画布中确认结构和交互,再集中实现与部署。
**教训:**生产部署应该验证已经形成的设计决策,而不是代替设计过程。
2. 操作生产库前,先确认真实运行进程使用的数据源
生产数据库连接只存在于 ECS 上的 Next.js 进程环境变量中,项目目录没有
.env。最终通过阿里云 Cloud Assistant 在实例内读取真实进程环境,并复用实例内的 Prisma Client 执行操作。**教训:**不能根据本地配置猜测生产数据源;任何生产数据操作都必须先确认目标连接。
3. 安全守卫也可能破坏正常开发流程
本地开发服务启动后退出,根因不是 Next.js,而是 safe-delete 守卫拦截了
.next缓存清理。最终把开发缓存目录迁移到/tmp,将构建缓存与项目资产分离。**教训:**安全策略必须区分用户资产和可再生缓存,否则保护机制本身会成为故障源。
4. Skill Manifest 是外部契约,不能假设结构永远一致
工作台曾因前端读取顶层
inputSchema,而真实数据位于manifest.inputSchema,导致页面白屏。最终增加错误边界、Manifest 适配层和防御性判空。**教训:**外部 Skill 接入必须经过统一适配和运行时校验,不能让原始结构直接进入 UI。
5. 发布成功不等于生命周期已经结束
早期用户填写发布链接后,复盘页面仍然为空。根因是发布记录已经更新,但文章没有同步推进到复盘阶段。修复后,确认发布会同步完成状态流转,并保留对历史数据的兼容读取。
**教训:**工作流产品不能只验证单个接口成功,还必须验证业务对象是否真正进入下一个生命周期阶段。
我刻意没有让 AI 做的事
妙笔希望先证明一件事: