今日已更新 80 条资讯 | 累计 20052 条内容
关于我们

标签:#EV

找到 2950 篇相关文章

AI 资讯

--- title: Day 1: Starting My Web Dev Journey published: true description: Learning HTML from scratch ---

Hi everyone! I'm a tech enthusiast currently mastering web development and game development. I am building my foundation from scratch,learning on my phone with Sololearn and Mimo while writing my code completely on a tablet using an app called Acode . Why I'm Doing This I want to learn how to turn big ideas into reality, one line of code at a time. I'm starting with the absolute basics of the web: HTML. My First HTML Project Today, I built a multi-page setup right on my tablet. Here is the structure of my homepage ( index.html ): <!DOCTYPE html> <html lang= "en" > <head> <title> My First Blog Post </title> </head> <body> <h1> Day 1: Starting My Journey To Become A Web Developer </h1> <p> I officially started learning web development today! </p><h3> What I Learnt </h3><Some of the things I learnt today were: </ p ><ul><li> HTML code controls the structure of a webpage </li><li> HTML tags are used to add elements to a webpage </li><li> Elements like button and paragraph require container tags while elements like images and line break only require empty tags <li> Container tags consist of both opening and closing tags. </li><li> Some HTML tags known as Semantic tags </li></ul><h4> What I Built </h4><p> I started with my first project which is my portfolio website </p><h5> Looking Ahead </h5><p> Next I want to learn more on HTML and dive deeper to get a better understanding of HTML and later learn CSS and JavaScript to style and make my webpage more interactive <p> </body> </html> ``` What's Next? ​My next immediate goal is to learn more about HTML and to learn CSS so I can start styling these pages and making them look awesome. After that, I'll be diving into JavaScript and starting my first simple game development projects. ​I'm excited to document my progress here as I grow from a beginner into a software developer. If you have any tips for learning on a mobile device, let me know in the comments!

2026-07-14 原文 →
AI 资讯

My MCP Server Kept Crashing. Here's the Error Recovery Pattern That Saved It.

I spent three days wondering why my MCP server would just... stop. No crash logs. No error messages. Clients connected fine, then after a few hours, every tool call returned silence. Turns out the Model Context Protocol (MCP) spec doesn't force you to handle errors — it assumes you will. But the reference implementations are minimal. Your server starts healthy, then bit by bit, things go wrong. A network blip. A malformed tool argument. An external API timeout. And suddenly your AI agent is staring at a blank response. Here's the pattern I ended up with. It's not clever. It just works. The Fix Start with a wrapper around your tool handlers. Every MCP server framework has some kind of tool registration — this works for the official Python SDK, the TypeScript SDK, and most community frameworks: from mcp.server import Server from mcp.types import ErrorData , INTERNAL_ERROR , INVALID_PARAMS import traceback import json class ResilientMCPServer ( Server ): """ An MCP server that doesn ' t silently die. """ async def call_tool ( self , name : str , arguments : dict ): try : result = await super (). call_tool ( name , arguments ) return result except ( ConnectionError , TimeoutError ) as e : # Network-level issues — reconnect and retry self . _reconnect () return self . _error_response ( f " Connection lost while executing { name } : { e } " ) except ValueError as e : # Bad arguments from the client — tell them clearly return self . _error_response ( f " Invalid arguments for { name } : { e } " , code = INVALID_PARAMS ) except Exception as e : # Everything else — log, don't crash traceback . print_exc () return self . _error_response ( f " Tool { name } failed: { e } " , code = INTERNAL_ERROR ) def _error_response ( self , message : str , code : int = INTERNAL_ERROR ): return { " content " : [{ " type " : " text " , " text " : f " ERROR: { message } " }], " isError " : True } def _reconnect ( self ): """ Reset transport layer without restarting the server. """ # Your recon

2026-07-14 原文 →
AI 资讯

Building an AI-Powered Lead Qualification API with Next.js 15 and Gemini 3.5 Flash

Every business wants more leads. But the real challenge isn't generating them—it's identifying which leads deserve your team's attention first. Instead of manually reviewing every inquiry, we can build a simple AI-powered API that analyzes incoming leads and assigns a priority score automatically. In this article, I'll show a lightweight production-ready approach using Next.js 15 and Gemini 3.5 Flash. Project Structure app/ ├── api/ │ └── qualify/ │ └── route.ts ├── lib/ │ └── gemini.ts └── page.tsx API Route import { NextResponse } from "next/server"; export async function POST(req: Request) { const { company, message } = await req.json(); const prompt = ` Company: ${company} Message: ${message} Give: - Score (1-100) - Priority - Reason `; // Call Gemini API here return NextResponse.json({ success: true, score: 92, priority: "High" }); } Example Response { " score " : 92 , " priority " : " High " , " reason " : " Large company with a clear automation requirement. " } Now your CRM, chatbot, or automation workflow can instantly decide which leads should be contacted first. Why This Matters A simple AI scoring layer can help teams: Reduce manual lead review Respond faster to high-value prospects Prioritize enterprise customers Improve sales efficiency Save hours every week The best part is that this API can be connected to forms, chatbots, CRMs, or n8n workflows without changing your existing process. Production Tips Before deploying this to production, make sure you: Validate incoming requests Store API keys securely Add rate limiting Log AI responses for monitoring Cache repeated requests where appropriate Small improvements like these make a huge difference once traffic starts growing. Final Thoughts AI shouldn't replace your sales team—it should remove repetitive work so they can focus on conversations that actually matter. A lightweight lead qualification API is one of the fastest AI features you can add to an existing product, and it scales well as your business

2026-07-14 原文 →
AI 资讯

LLM Evaluation System Prompts Scored Rubrics Runtime Guardrails: A Practical Guide for Production

LLM Evaluation System Prompts Scored Rubrics Runtime Guardrails: A Practical Guide for Production Learn how to evaluate LLM outputs in production using system prompts, scored rubrics, and runtime guardrails to prevent hallucinations and ensure quality. TL;DR: To evaluate LLM outputs in production, combine system prompts that define evaluation criteria, scored rubrics using LLM-as-a-judge for dimensions like correctness and relevance, and runtime guardrails that filter or flag unsafe outputs. This approach scales better than human review, adapts via prompt changes, and catches failures that status codes miss, as seen in the Air Canada chatbot case. Why Production LLM Evaluation Demands More Than Status Codes A 200 status code only confirms the server processed the request—it says nothing about whether the generated text is factual, safe, or useful. The Air Canada chatbot that invented a non-existent bereavement discount returned perfectly valid HTTP responses, yet the hallucinated policy led to a tribunal ruling against the airline. Production evaluation must therefore separate operational health (latency, error rates) from output quality (correctness, relevance, harmlessness). Consider a typical API call that succeeds operationally but fails qualitatively: import requests response = requests . post ( " https://api.example.com/v1/chat " , json = { " model " : " gpt-4o " , " messages " : [{ " role " : " user " , " content " : " What is Air Canada ' s bereavement policy? " }]}, headers = { " Authorization " : " Bearer $KEY " } ) print ( response . status_code ) # 200 print ( response . json ()[ " choices " ][ 0 ][ " message " ][ " content " ]) # Output: "Air Canada offers full refunds for bereavement-related cancellations..." A 200 status code and a well-formed JSON body mask a completely fabricated policy. To catch this, you need a separate evaluation layer that scores the output against a rubric. LLM-as-a-judge is a common approach, using a second model to assess the

2026-07-14 原文 →
AI 资讯

Verify a Self-Hosted Installer Before Running It as Root

Downloading an installer and immediately executing it as root collapses three operational decisions into one command: Which artifact? -> Did these bytes arrive intact? -> Should this host execute them? Separate those decisions and the install becomes reviewable, reproducible, and recoverable. A concrete source-review boundary At commit c58bcd4 , the MonkeyCode runner installation template selects x86_64 or aarch64 , checks AVX on x86, requires root, and downloads an architecture-specific installer before executing it. The reviewed template uses curl -4sSLk , so certificate verification is disabled by -k . It also downloads an unversioned path. I could not find a pinned version, digest, or signature check in that template. That is a statement about controls visible in one pinned file—not a claim that the release service is compromised or that no external release control exists. Put a manifest before execution For each release artifact, publish immutable metadata through a separately protected release process: { "version" : "1.2.3" , "architecture" : "x86_64" , "file" : "runner-installer-1.2.3-x86_64" , "sha256" : "<64 lowercase hex characters>" , "size" : 18439210 , "rollback" : { "previous_version" : "1.2.2" , "artifact" : "runner-installer-1.2.2-x86_64" } } SHA-256 detects bytes that differ from the manifest. It does not prove who authored the manifest. Serve the manifest over validated TLS, pin it through deployment configuration, or sign it and verify the signature with a trusted offline public key. Verify as an unprivileged staging step The companion verify-installer.mjs checks filename, exact size, digest, version, architecture, and rollback metadata: node verify-installer.mjs release-manifest.json fixture-installer.sh node test-verifier.mjs Expected output uses the fixture's actual digest: PASS 1.2.3-fixture sha256=<digest> PASS verified fixture; rejected tampered artifact before execution The negative test appends a line to the artifact and requires both size

2026-07-14 原文 →
AI 资讯

Add Arrow-Key Shortcuts to a Confirmation Dialog Without Breaking Accessibility

Two buttons in a confirmation dialog look simple: Cancel and Confirm. Keyboard behavior makes the component a small state machine. A recent MonkeyCode change gives us a concrete example. Issue #862 and PR #863 add these shortcuts to the slash-command confirmation: ArrowLeft -> focus Cancel ArrowRight -> focus Confirm The reviewed implementation at commit c58bcd4 moves focus through button refs. That is a useful extra interaction. It is not a replacement for the dialog's accessibility foundation. Keep the baseline first For an alert-style confirmation, users still need: an accessible name and description; focus moved inside when the dialog opens; Tab and Shift+Tab constrained to dialog controls; Escape to dismiss when cancellation is allowed; visible focus; focus returned to the trigger after close; actual buttons whose labels explain the actions. The WAI-ARIA Authoring Practices Alert Dialog Pattern describes the modal semantics and keyboard foundation. Left/right mapping is a product shortcut, not a required AlertDialog convention. That means we must not steal keys from the established behavior around it. Isolate the extra mapping The companion keyboard.mjs starts with a pure function: export function arrowAction ( key ) { if ( key === " ArrowLeft " ) return " cancel " ; if ( key === " ArrowRight " ) return " confirm " ; return null ; } The event handler ignores unrelated and modified keys: export function handleDialogArrow ( event , controls ) { const action = arrowAction ( event . key ); if ( ! action || event . altKey || event . ctrlKey || event . metaKey ) return false ; event . preventDefault (); controls [ action ]. focus (); return true ; } Notice what is absent: no handler for Tab , Shift+Tab , Escape , or Enter . The native <dialog> and buttons in the minimal demo retain their normal jobs. In a React component, use a well-tested modal/dialog primitive for focus containment and dismissal, then add this narrow handler to its content. A complete minimal dialo

2026-07-14 原文 →
开发者

I Added 200+ Languages to a Translator… Then Realized Language Wasn't the Hardest Part

I'll Be Honest: The Internet Already Has Translators I know. Language translation isn't a new idea. There are already huge translation platforms out there. So when I started working on a translator for my tools website, I wasn't thinking: "I'm going to reinvent translation." Not at all. My thought was much simpler: "Can I make quick translation feel less distracting?" My Frustration Was Actually Pretty Simple Sometimes I just need to translate text. That's it. I don't want to: Create an account Open five different menus Break a long text into tiny pieces Jump between multiple tools I want to paste the text... Choose a language... And get the translation. So I Built My Own Version 👉 https://allinonetools.net/language-translator/ The tool currently supports 200+ languages and language variations . You can: Detect the source language Select the target language Translate long text Upload text Use voice input Listen to the result Copy or share the translation And I wanted to keep the text experience simple without forcing users into tiny input limits. Just: Enter → Choose Language → Translate 200+ Languages Sounded Simple Until I Saw the List English. Hindi. Gujarati. Spanish. Arabic. These are the languages most people immediately think about. But then I started going through the full language list. Abkhaz. Acholi. Afar. Alur. Aymara. Baluchi. And many more. Honestly... I hadn't even heard of some of them before building this. That was probably my biggest learning moment. I Realized How Small My Own View of the Internet Was As a developer, it's easy to build around the languages we personally know. For me, seeing English, Hindi, and Gujarati feels normal. But the internet is much bigger than my own experience. Someone somewhere may be trying to understand a sentence in a language I've never even heard spoken. That changed how I looked at this tool. The Hard Part Wasn't Adding a Dropdown A dropdown with 200+ options looks impressive. But that's not the real problem. The

2026-07-14 原文 →
AI 资讯

Speed Test: I Found AI APIs 99% Cheaper Than Premium

Here's the thing: speed Test: I Found AI APIs 99% Cheaper Than Premium I have a confession: I've been overpaying for AI APIs for years. Like, embarrassingly overpaying. When I finally sat down and actually benchmarked 15 different models on speed and cost, I couldn't believe what I found. Some of the fastest models out there cost literal pennies per million tokens. Here's the thing — if you're still defaulting to whatever the big labs are pushing, you're leaving serious money on the table. So I spent a week running tests through Global API's infrastructure, hitting endpoints from multiple regions, and crunching numbers until my eyes hurt. What I discovered genuinely surprised me. Check this out: there's a model that pushes 80 tokens per second and costs $0.15 per million output tokens. Compare that to premium options charging $3.00/M and you'll understand why I had to write this down. Let me walk you through everything I found. Why I Even Started This Whole Thing My monthly AI bill got out of control. I'm running a few production apps that do text generation, summarization, and chat, and my December bill made me physically flinch. I knew there had to be faster, cheaper models hiding in the ecosystem — I just hadn't taken the time to actually measure them properly. That's the whole reason I ran these benchmarks. Not for clout, not for content marketing. Pure self-interest. I wanted to know where the actual sweet spots are. Where you get the best speed-per-dollar ratio. Where you can save 70%, 80%, even 99% without tanking your user experience. What I found was honestly kind of shocking. My Testing Setup (For the Nerds) I kept the methodology tight and consistent. Here's exactly how I ran everything: Date: May 20, 2026 Test regions: US East (Ohio) and Asia (Singapore) Prompt: "Explain recursion in 200 words" Output target: ~150 tokens per run Iterations: 10 runs each, averaged the results Streaming: Enabled via SSE Base URL: https://global-apis.com/v1 I measured two k

2026-07-14 原文 →
AI 资讯

ADR Template: How AI Generates Architecture Decision Records Your Future Self Will Thank You For

Teams make dozens of architectural decisions every month but document almost none of them. The rest dissolve into Slack threads, hallway conversations, and the minds of people who will leave the company within a year. Six months later, a new developer stares at the code and asks: "Why Redis here instead of PostgreSQL for queues?" Nobody remembers. An archaeological dig through Git history, Slack, and Notion begins. Two hours spent investigating a decision that originally took 15 minutes. Architecture Decision Records (ADRs) solve this problem. But they don't get written. The reason is simple: drafting an ADR takes 30-40 minutes, and the developer has already moved on to the next task. AI compresses that to 3-5 minutes. This article covers ADR structure, prompts for LLM-based generation, real-world examples, and CI pipeline automation. What ADRs are and why capturing architectural decisions matters An ADR (Architecture Decision Record) is a document that captures one specific architectural decision. Not a spec, not an RFC, not a design document. One decision, one file. Michael Nygard introduced the concept in 2011. The format took hold at large companies (Spotify, Thoughtworks, GitHub) but remains rare in smaller teams. The main reason: the writing overhead feels higher than the value it delivers. Three situations where the absence of ADRs hurts the most: Onboarding. A new developer reads the code and encounters an unconventional decision. Without an ADR, they either spend hours investigating, or treat it as a mistake and "fix" it. Both paths are expensive for the team. Revisiting decisions. Context changes: load increases, new requirements emerge, a dependency goes stale. Without a record of why the current solution was chosen and which alternatives were rejected, the team re-runs the entire analysis from scratch. Audits and compliance. In regulated industries (fintech, healthtech), architectural decisions require documented justification. ADRs close that gap automa

2026-07-14 原文 →
AI 资讯

A variable I'd refactored into one function — and kept referencing from another. Python's lazy evaluation hid it, and an AST test finally caught it

One day the browser automation flow started failing right after plugin updates with NameError: name 'plugin_form_selectors' is not defined in the post-update "residual check" step. The refactor that introduced this had landed back in v1.6.1. The error didn't surface until many rounds later. Reading the code, the cause is obvious in seconds — but nobody hit it for ages, because Python's lazy evaluation kept the leftover reference hidden until exactly the right execution path ran. This post walks through what the bug was and how we structurally prevented its kind via an AST static-analysis test. What happened — a reference that crossed a scope boundary browser_utils.py has two functions involved: run_browser_update_flow() , which orchestrates the whole update flow, and browser_update_remaining_plugins() , which handles only the plugin-update logic. The list of plugin-form selector candidates, plugin_form_selectors , used to be a local variable inside run_browser_update_flow() . In the v1.6.1 refactor — "let's split plugin update into its own function" — we created browser_update_remaining_plugins() and moved the plugin_form_selectors definition into it . # After v1.6.1 refactor def browser_update_remaining_plugins ( page , site , update_url ): plugin_form_selectors = [ # ← defined here ' #update-plugins-table-wrap form ' , ' form[name= " upgrade-plugins " ] ' , ' form[action*= " do-plugin-upgrade " ] ' , ' .plugins-php form ' , ] # ... update logic ... def run_browser_update_flow ( site , page ): # ... call to plugin updater ... browser_update_remaining_plugins ( page , site , update_url ) # ★ post-update "residual check" still uses the old local name for selector in plugin_form_selectors : # NameError if page . locator ( selector ). count () > 0 : pending_browser . append (...) The " after updating, make sure no plugin update forms are still visible " residual check stayed in run_browser_update_flow() . During the refactor, the call to extract this loop alongside the

2026-07-14 原文 →
AI 资讯

SilentShare — A Browser-Based Peer-to-Peer File Sharing App

Have you ever been in a computer lab, classroom, or office where you needed to quickly send a file between your phone and laptop? I run into this problem all the time. Sometimes there's no USB cable, no pendrive, Bluetooth is painfully slow, or uploading to cloud storage just to download the file on another device feels unnecessary. So I decided to build SilentShare . What is SilentShare? SilentShare is a browser-based peer-to-peer file sharing application that lets you instantly share: 📁 Files (up to 50 MB) 💻 Code snippets 📝 Text 🖼️ Images No installation. No account. No server storing your files. Your data goes directly from one device to another using WebRTC . Whether you're sending files from your phone to your laptop, between classmates, or across the internet, SilentShare keeps the process simple. Why I Built It I wanted something that: Opens instantly in any browser Doesn't require creating an account Doesn't upload files to someone else's server Works on desktop and mobile Feels lightweight and fast Instead of relying on cloud storage, I wanted the browser itself to become the transfer tool. Features ✨ Peer-to-peer file transfer using WebRTC 📂 File sharing up to 50 MB (including ZIP files) 🔒 Optional end-to-end encrypted rooms using AES-GCM 📷 QR code invitations with built-in camera scanner 📊 Live progress, transfer speed, ETA, pause & resume 🖼️ Preview support for: Images Audio Video PDFs 💻 Share code snippets with syntax highlighting 👥 Multi-user rooms (around 5 participants) 🌙 Dark & Light mode 📱 Installable as a Progressive Web App (PWA) How It Works Create a room Receive a random room code Share the code, QR code, or invite link Other devices join Start sharing instantly The files are transferred directly between devices instead of passing through a storage server. Privacy One of the goals of SilentShare was privacy. No user accounts No cloud storage No permanent database Nothing stored after the browser tab closes If you set a room password, all transf

2026-07-14 原文 →
AI 资讯

I Built an AI-Powered CLI That Migrates Legacy Java Code to Java 17/21/25

I Built an AI-Powered CLI That Migrates Legacy Java Code to Java 17/21/25 If you've spent any time in enterprise Java, you know the feeling. You open a service that's been running since 2014 and you're greeted by walls of anonymous inner classes, verbose null checks, Collections.unmodifiableList wrapping a new ArrayList , and switch statements with more break keywords than actual logic. Individually each pattern takes 30 seconds to fix. But across a codebase with 300 files, it's a week of mechanical work — and that's before you factor in the code review. So I built java-migrate : a CLI tool that scans your Java files, detects legacy patterns, and sends them to Claude with a precise system prompt to get them modernised. One command, instant diff, no surprises. What it looks like in practice Here's a typical legacy file before migration: public class LegacyService { // POJO with getters/setters public static class User { private String name ; private int age ; public String getName () { return name ; } public void setName ( String name ) { this . name = name ; } public int getAge () { return age ; } public void setAge ( int age ) { this . age = age ; } } public List < User > sortUsers ( List < User > users ) { // Anonymous Comparator users . sort ( new Comparator < User >() { @Override public int compare ( User a , User b ) { return a . getName (). compareTo ( b . getName ()); } }); return Collections . unmodifiableList ( users ); } public String describeObject ( Object obj ) { // instanceof + cast if ( obj instanceof String ) { String s = ( String ) obj ; return "String of length " + s . length (); } return "Unknown" ; } public String classify ( int value ) { // switch statement String result ; switch ( value ) { case 1 : result = "one" ; break ; case 2 : result = "two" ; break ; default : result = "other" ; } return result ; } } Run java-migrate LegacyService.java --dry-run --verbose and you get this diff: - public static class User { - private String name; - privat

2026-07-14 原文 →
AI 资讯

It works on my machine, but is it working for my users?

Every time I shipped something, the same thought hit me a few hours later: It works on my machine. It works in staging. But is it actually working for the people using it right now? I had analytics. I had a green dashboard. And I still had no honest answer to that question. Users would quietly leave, a button would silently break on Safari, a page would crawl on a mid-range Android, and I'd find out days later, if at all. That gap is what I ended up building HeronSignal to close. But before I talk about the tool, let me talk about the pain, because I think you've felt at least one version of it. The pain, depending on who you are If you're a vibe coder / solo builder You ship fast. Cursor, Claude, v0, a Vercel deploy, and it's live. Beautiful. Then… nothing. You have no idea what happens after "Deploy successful." Is the checkout button throwing an error on mobile? Is your landing page slow enough that half your visitors bounce before it paints? You don't know, because setting up "real" monitoring feels like a second job: a Datadog dashboard you'll never look at, a Sentry config you half-finish. So you just… hope. And hope is not a monitoring strategy. If you're an engineer Your problem isn't no data. It's too much . Ten dashboards, alert fatigue, a Sentry inbox with 400 issues where 390 are noise. Something's clearly wrong, but which thing actually matters? You spend your morning triaging instead of fixing. And when you finally pick an error, you get a stack trace with zero context: no idea what page it happened on, what the user was doing, or how to reproduce it. Triage is not the job. Fixing is the job. But the tools make you do the triage first. If you're a product person You can see in your funnel that people drop off at step 3. What you can't see is why . Was it a JS error? A slow page? A confusing layout? Your analytics tool tells you what happened but never why , and the engineering dashboards that might explain it are unreadable walls of numbers. So you gue

2026-07-14 原文 →
AI 资讯

Audit-log every email your AI agent sends

When an autonomous agent gets an email address of its own, the first question your security team asks isn't "can it send mail?" It's "can you prove, six months from now, exactly what it said and to whom?" That's a different problem from "does it work." A demo that fires off a few support replies looks great in a sprint review. But the moment a real customer says "your bot promised me a refund," or a regulator asks for the complete record of what an automated system told a data subject, you need a defensible trail — an immutable record of every outbound and inbound message the agent touched, captured outside the mailbox the agent can also delete from. I work on the Nylas CLI, so the terminal commands below are the exact ones I reach for. But the architectural point here is provider-agnostic and it's the part most "AI email" tutorials skip: the live mailbox is not your audit log. It's mutable, it has retention limits, and the same agent that sends mail can also trash it. If your only record of what the agent did lives in the inbox, you don't have an audit trail — you have a working copy. What "audit-log everything" actually means There are two stores in this design, and keeping them separate is the whole point. The live mailbox — the Agent Account grant. Messages flow in and out here. It's queryable, it's real-time, and it's mutable . Flags change, messages move folders, things get trashed. On the free plan it's also retention-limited: 30 days for the inbox, 7 days for spam. The audit store — your system. An append-only, write-once log keyed by message_id and thread_id . Nothing in it is ever updated or deleted in normal operation. This is the record you hand a reviewer. The audit store is the thing you build. Nylas gives you the two capture points — the send response and the inbound webhook — but the immutability is your responsibility. That means a WORM (write-once-read-many) object store, an append-only table with no UPDATE / DELETE grant for the app role, or a has

2026-07-14 原文 →
AI 资讯

One agent mailbox per tenant in a multi-tenant SaaS

Most multi-tenant SaaS apps that send email do it from one shared identity. There's a notifications@yourapp.com , every customer's mail flows through it, and the tenant is just a from_name you stamp on the subject line or a footer you swap out. That's fine until it isn't — until Tenant A's spam complaints drag down Tenant B's deliverability, until a reply from a customer lands in a single firehose inbox you now have to fan back out, until one tenant wants a stricter send cap than another and you realize you built none of that into the data model. So let's not share. Let's give every tenant its own real mailbox — a dedicated Agent Account per customer, each with its own grant_id , its own send identity, its own policy and limits, grouped into its own workspace. Not one inbox with a thousand label hacks. A thousand inboxes, isolated by construction. I work on the Nylas CLI, so the terminal commands below are the exact ones I reach for when I'm wiring this up. Every step gets the two-angle tour: the raw curl call and the nylas command that does the same thing. Why per-tenant beats one shared sender The shared-sender model fails along a few predictable seams. Per-tenant Agent Accounts close each one: Deliverability blast radius. When everyone sends from one address, one tenant's bounce rate and spam complaints poison the reputation everyone shares. Per-tenant accounts — and, if you want, per-tenant domains — keep one customer's bad behavior from sinking the rest. Inbound that actually belongs to someone. A shared sender means replies come back to one mailbox and you're left correlating them to tenants by hand. When each tenant has its own grant, an inbound message.created event already carries the grant_id . The routing is done before your handler runs. Per-tenant policy and limits. Different customers, different rules. A trial tenant capped at a low daily send; an enterprise tenant with a higher quota and longer retention. With a shared sender you'd build all of that y

2026-07-14 原文 →
AI 资讯

Spin up ephemeral test inboxes for email integration tests

Most teams test email by not testing it. The send path gets a mock — expect(transport.send).toHaveBeenCalledWith(...) — and everyone agrees that's "good enough." The receive path gets skipped entirely, because there's no honest way to assert on a real inbox from a test runner. So the one part of your system that talks to the outside world over an unreliable, asynchronous, third-party channel is the part with the least coverage. That's backwards. The reason email is hard to test isn't the sending. It's the asserting . You can fire POST /messages/send all day, but to prove the message actually left, rendered correctly, and arrived with the body you expected, you need a real mailbox you control — one you can read programmatically and throw away when the run finishes. Shared Gmail test accounts almost get you there, but they bring OAuth on the runner, catch-all races between parallel workers, and a 90-day token that expires the night before a release. This post is about a different fixture: a disposable Agent Account created at the start of a CI run and deleted at the end. You mint a real mailbox per run (or per test), point your application at it, send and receive real mail, assert on the actual message body, and tear the whole thing down. No OAuth. No shared inbox. No leftover state. What an Agent Account gives you here An Agent Account is just a Nylas grant with a grant_id . That's the whole trick, and it's worth saying plainly because it's what makes this pattern cheap: an Agent Account works with every grant-scoped endpoint you already know — Messages, Drafts, Threads, Folders, Attachments, Webhooks. There's nothing new to learn on the data plane . If you've ever called GET /v3/grants/{grant_id}/messages , you already know how to read a test inbox. The difference from a normal grant is provisioning. A regular grant needs a real human to complete an OAuth flow. An Agent Account is created with a single API call — no OAuth screen, no refresh token, no human. It's a m

2026-07-14 原文 →
AI 资讯

Require human approval before your agent sends email

Most "AI email agent" demos end with a triumphant send . The model writes a reply, the code POSTs it, and a real message lands in a real stranger's inbox. That's a great demo and a terrible production default. The moment your agent can send mail with nobody watching, you've handed an LLM a corporate email address and the standing authority to use it. One hallucinated price, one confidently wrong refund promise, one apology to the wrong customer, and you're explaining to legal why a bot signed an email as your company. There's a boring, durable fix that predates AI by decades: don't send — draft. Stage the message, put a human in front of it, and only send once someone with a name and a pulse approves. Email systems have had a "Drafts" folder forever for exactly this reason. The Nylas Drafts API turns that folder into something better — an approval queue your agent writes into and your reviewers drain. This post builds that queue. The agent creates a draft, a human reviews the pending drafts, and an approved draft gets sent byte-for-byte unchanged . No re-rendering, no "the agent regenerates it on approval" race where the thing you approved isn't the thing that ships. I work on the Nylas CLI, so the terminal commands below are the exact ones I reach for, and I'll pair every one with the raw curl so you can wire it into a backend in whatever language you like. This is deliberately not about escalating inbound threads to a human (that's a different problem, where the trigger is a message arriving). Here the trigger is the agent wanting to send , and the gate sits on the outbound path. Why a draft is the right approval primitive You could build approval a dozen ways. You could buffer the agent's output in a queue table and call send later. You could stash a JSON blob in Redis. Both work, and both quietly reinvent something the email stack already gives you. A draft is a real, persisted email object , on the mailbox, with a stable id . That buys you three things a homegr

2026-07-14 原文 →